Metadata-Version: 2.4
Name: botshield
Version: 0.3.0
Summary: BotShield -- layered bot & fraud request-filtering pipeline (signals -> Tier 1 rules -> Tier 2 scoring -> logging -> optional ML)
License-Expression: Apache-2.0
License-File: LICENSE
Requires-Python: >=3.11
Requires-Dist: pyyaml>=6.0
Requires-Dist: requests>=2.31
Provides-Extra: agent
Requires-Dist: openai>=1.50; extra == 'agent'
Requires-Dist: playwright>=1.47; extra == 'agent'
Requires-Dist: python-dotenv>=1.0; extra == 'agent'
Requires-Dist: requests>=2.31; extra == 'agent'
Provides-Extra: dashboard
Requires-Dist: pandas>=2.2; extra == 'dashboard'
Requires-Dist: streamlit>=1.35; extra == 'dashboard'
Provides-Extra: dev
Requires-Dist: httpx>=0.27; extra == 'dev'
Requires-Dist: pytest>=8.0; extra == 'dev'
Provides-Extra: django
Requires-Dist: django>=4.2; extra == 'django'
Provides-Extra: flask
Requires-Dist: flask>=3.0; extra == 'flask'
Provides-Extra: ml
Requires-Dist: joblib>=1.4; extra == 'ml'
Requires-Dist: pandas>=2.2; extra == 'ml'
Requires-Dist: scikit-learn>=1.4; extra == 'ml'
Provides-Extra: mongodb
Requires-Dist: pymongo>=4.6; extra == 'mongodb'
Requires-Dist: python-dotenv>=1.0; extra == 'mongodb'
Provides-Extra: redis
Requires-Dist: redis>=5.0; extra == 'redis'
Provides-Extra: web
Requires-Dist: fastapi>=0.111; extra == 'web'
Requires-Dist: uvicorn[standard]>=0.30; extra == 'web'
Description-Content-Type: text/markdown

# BotShield

A layered bot/fraud request-filtering pipeline:

```
request -> your app's middleware (this package)
         -> HTTP call to a running botshield.evaluate_service
              -> Tier 1: fast rules engine
              -> Tier 2: risk scoring (weighted signals, optionally ML-assisted)
         -> action: allow / challenge / block
```

This package is a **thin client** -- like the official SDK for a hosted
API. It contains no detection logic itself: Tier 1 rules, Tier 2 scoring,
signal extraction, and event logging all run inside a separate
`evaluate_service` process (self-hosted from the project's git repository,
or a hosted instance someone gives you a URL and token for). What you get
here is the piece that talks to that service and enforces its verdict in
your own FastAPI, Flask, or Django app.

## What's in this package

```
botshield/
  config.py           BotShieldConfig -- service_url/service_token/clearance_secret/fail_mode
  types.py             RequestData dataclass (shared shape used internally)
  client_ip.py         trusted-proxy-aware real client IP + TLS fingerprint resolution
  session.py           per-visitor botshield_sid correlation cookie
  clearance.py         local clearance-cookie check (skips a network round trip
                        once a visitor has already passed the challenge gate)
  challenge_page.py     renders the challenge gate's HTML+JS page
  remote_client.py      HTTP client for the evaluate service's /botshield/evaluate
  decide.py             ties the above together into one allow/challenge/block decision
  verify_handler.py     proxies the challenge page's solve attempt to the evaluate service
  middleware_{fastapi,flask,django}.py   framework adapters
  routes_{fastapi,django}.py             wiring for the POST /botshield/verify endpoint
```

## Install

```
pip install botshield          # core client
pip install "botshield[web]"   # + FastAPI adapter deps
pip install "botshield[flask]" # + Flask adapter deps
pip install "botshield[django]" # + Django adapter deps
```

## Usage (FastAPI)

```python
import os

from fastapi import FastAPI
from botshield import BotShieldConfig
from botshield.middleware_fastapi import BotShieldMiddleware
from botshield.routes_fastapi import build_verify_router

config = BotShieldConfig(
    service_url="http://127.0.0.1:9000",             # where evaluate_service is running
    service_token=os.environ["BOTSHIELD_SERVICE_TOKEN"],       # must match that service
    clearance_secret=os.environ["BOTSHIELD_CHALLENGE_SECRET"], # must match that service
    fail_mode="open",  # "open" | "closed" -- what to do if the service is unreachable
)

app = FastAPI()
app.add_middleware(BotShieldMiddleware, config=config)
app.include_router(build_verify_router(config))  # needed for the challenge gate
```

Flask (`botshield.middleware_flask.init_app(app, config)`) and Django
(`botshield.middleware_django.set_config(config)` + `BotShieldDjangoMiddleware`
in `MIDDLEWARE`) follow the same shape -- see each module's docstring.

## The challenge gate

When a request scores into the challenge band, the middleware serves a
small HTML+JS page instead of the real response, asking the browser to
solve a proof-of-work puzzle and submit a fingerprint bundle. Only when
both are accepted by the evaluate service does the client get a signed,
time-limited clearance cookie (checked locally by this package on
subsequent requests, no extra network round trip) and get to reload the
original URL.

### Privacy note: the session cookie

This package issues an opaque `botshield_sid` cookie to **every** visitor
on first contact (not only challenged ones), so the evaluate service's
Tier 2 scoring can correlate a session's history even on a first "allow."
It's httponly, samesite=lax, and holds no personal data -- but it is a
persistent identifier set for 100% of traffic, so a real deployment very
likely needs to disclose it in a cookie-consent banner under GDPR/ePrivacy.

## Fail-open vs. fail-closed vs. unauthorized

- `fail_mode="open"` (default): if the evaluate service is unreachable
  (network error, timeout, 5xx), requests are allowed through so an outage
  in the detection layer doesn't take your whole site down.
- `fail_mode="closed"`: the same failures instead return HTTP 503.
- An invalid, revoked, or exhausted `service_token` is **never** treated as
  fail-open, regardless of `fail_mode` -- it always returns HTTP 402. A bad
  token means "this deployment isn't configured/licensed correctly," not
  "the service had a bad moment," so it must always block. See
  `decide.py`'s docstring.

## License

Apache-2.0
