Metadata-Version: 2.4
Name: zerov4
Version: 0.1.2
Summary: Request-level protection for WSGI applications: signatures, rate limiting, honeypots, IP reputation and a bot challenge, inspected before your app sees the request.
Author: Barış
License: Apache-2.0
Project-URL: Homepage, https://github.com/brsx-labs/zerov4
Project-URL: Issues, https://github.com/brsx-labs/zerov4/issues
Keywords: security,waf,firewall,wsgi,flask,rate-limiting,bot-detection,honeypot,intrusion-detection
Classifier: Development Status :: 4 - Beta
Classifier: Environment :: Web Environment
Classifier: Intended Audience :: Developers
Classifier: Intended Audience :: System Administrators
Classifier: License :: OSI Approved :: Apache Software License
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Internet :: WWW/HTTP :: WSGI :: Middleware
Classifier: Topic :: Security
Classifier: Topic :: System :: Networking :: Firewalls
Requires-Python: >=3.9
Description-Content-Type: text/markdown
License-File: LICENSE
License-File: NOTICE
Requires-Dist: werkzeug>=2.0
Provides-Extra: redis
Requires-Dist: redis>=4.0; extra == "redis"
Provides-Extra: geoip
Requires-Dist: geoip2>=4.0; extra == "geoip"
Provides-Extra: prod
Requires-Dist: waitress>=2.0; extra == "prod"
Provides-Extra: toml
Requires-Dist: tomli>=2.0; python_version < "3.11" and extra == "toml"
Provides-Extra: all
Requires-Dist: redis>=4.0; extra == "all"
Requires-Dist: geoip2>=4.0; extra == "all"
Requires-Dist: waitress>=2.0; extra == "all"
Requires-Dist: tomli>=2.0; python_version < "3.11" and extra == "all"
Provides-Extra: dev
Requires-Dist: pytest>=7.0; extra == "dev"
Requires-Dist: flask>=2.0; extra == "dev"
Requires-Dist: build; extra == "dev"
Requires-Dist: twine; extra == "dev"
Dynamic: license-file

# ZeroV4

Request-level protection for WSGI applications. Signatures, rate limiting, honeypots, IP reputation, and a bot challenge — all applied **before** your app sees the request.

```python
from flask import Flask
from zerov4 import arx

app = Flask(__name__)

@app.route("/")
def index():
    return "my site"

arx.run(app)          # asks for a port, then serves — protected
```

ZeroV4 asks for a port, a debug flag and a protection level, then serves your app with every request inspected first.

---

## Install

```bash
pip install zerov4
```

Optional extras:

```bash
pip install zerov4[redis]   # Redis-backed storage
pip install zerov4[geoip]   # country lookup for blocked IPs
pip install zerov4[prod]    # waitress, for serving in production
pip install zerov4[all]
```

Core dependency: `werkzeug`. That's the whole list. A security package that pulls in half of PyPI has a larger attack surface than the attacks it stops.

### Early development

ZeroV4 moves fast right now. Check for updates before you rely on it:

```bash
pip install --upgrade zerov4
```

In production, pin the version and upgrade deliberately. This is a security
layer sitting in front of your app — an unplanned update is its own kind of
outage.

---

## Your first run

Make a file called `run.py`:

```python
from flask import Flask
from zerov4 import arx

app = Flask(__name__)

@app.route("/")
def index():
    return "<h1>my site</h1>"

@app.route("/search")
def search():
    from flask import request
    return f"you searched: {request.args.get('q')}"

arx.run(app)
```

Run it:

```bash
python run.py
```

Answer the questions (Enter accepts every default), then open <http://localhost:5000>:

| Try this | You should get |
|---|---|
| `/` | your page, normally |
| `/search?q=hello` | your page, normally |
| `/search?q=union select 1` | `429` — blocked before your route ran |
| `/.env` | `403` — honeypot, instant ban |
| `/panel` | the SOC panel |

**Banned yourself while testing?** That is the system working. Let yourself back in:

```bash
python -c "from zerov4 import arx; arx.unblock('127.0.0.1')"
```

Or delete the `logs/` folder to reset everything.

> Throughout this README, `myapp` means *your own file*, e.g. `myapp.py` in
> the same folder. It is not something you install. If your app lives in
> `shop.py` as `app`, then it's `from shop import app` and `arx.run("shop:app")`.
> Avoid naming your file after a package that already exists on PyPI —
> Python will import that one instead of yours.

---

## Usage

### The quick way

If your app is in the same file, hand it over directly:

```python
from zerov4 import arx

arx.run(app)                 # menu, then serve
arx.run(app, port=8080)      # port answered, so not asked
arx.run(interactive=False)   # no questions at all
```

If it lives in another file — say `myapp.py`, with the app named `app` —
import it, or let ZeroV4 do the importing:

```python
from myapp import app       # your file, in this folder
arx.run(app)
```

```python
arx.run("myapp:app")        # module:attribute, same thing
```

### Under gunicorn

`run()` serves the app itself. If gunicorn/uwsgi is doing that, wrap instead:

```python
# wsgi.py
from zerov4 import arx
from myapp import app

application = arx.wrap(app)
```

```bash
gunicorn wsgi:application -w 4
```

### As middleware

```python
from flask import Flask
from zerov4 import ZeroGuard, make_config

app = Flask(__name__)
app.wsgi_app = ZeroGuard(app.wsgi_app, cfg=make_config("balanced"))
```

It's plain WSGI, so Django, Bottle, Pyramid and anything else that speaks the protocol work the same way.

### From the shell

```bash
zerov4 init              # configure interactively, write zerov4.toml
zerov4 run myapp:app     # serve — myapp.py in this folder, app inside it
zerov4 status            # what the firewall knows
zerov4 unblock 1.2.3.4   # let an address back in
zerov4 whitelist 10.0.0.5
zerov4 signatures        # what's loaded
zerov4 feedback events   # every block/throttle, with an id
zerov4 feedback wrong ID # mark one as a false positive
```

Run these from the folder your app is in — `zerov4 run` imports it the same
way Python would.

---

## Protection levels

| | `permissive` | `balanced` *(default)* | `paranoid` |
|---|---|---|---|
| Ban threshold | 20 hits | 10 hits | 10 hits |
| Rate limit | 300 / 10s | 100 / 10s | 10 / 1s |
| 404s count as attacks | no | no | **yes** |
| Non-JSON body is an attack | no | no | **yes** |
| Fuzzy keyword matching | no | no | **yes** |
| Session bound to IP | no | no | **yes** |
| Bot challenge | off | on | on |
| Pattern analysis | off | on | on |
| Blocking throttle | no | no | **yes** |

**Balanced** is tuned for a site with real visitors and is what you want unless you have a reason.

**Paranoid** is the original Autonomous Agent Zero-V4 behaviour, kept intact. It treats ordinary traffic as hostile: every 404, every HTML form post, and fuzzy keyword matches all count toward a ban. Mobile users switching from WiFi to cellular change IP, which paranoid reads as session theft. It is for a target you control, not a public site. ZeroV4 asks you to confirm before enabling it.

---

## What it does

**Signatures.** Keyword matching plus stateful `detect()` functions that count across a time window. Keywords are word-bounded, so `select` does not match `/products/selectbox`.

**Escalating throttle.** Hit 1 costs 2 seconds, hit 2 costs 4, hit 9 costs 300 (capped). Hit 10 is a permanent ban. Throttling returns `429` with `Retry-After` immediately — it does not hold a worker asleep.

**IP reputation.** Banned addresses are remembered and blocked on sight next time, without earning hits again. Entries expire when they haven't been seen for a while, so an address that changed hands isn't blocked forever.

**Honeypots.** `/.env`, `/wp-admin`, `/phpmyadmin` and friends. One request is an instant ban.

**Bot challenge.** Throttled browsers get an arithmetic question plus pointer-movement analysis instead of a flat 429, so a real person can prove it and carry on. Touch devices are not punished for having no `mousemove`.

**Pattern analysis.** Correlates across IPs: coordinated attacks, slow reconnaissance, multi-category probing, rapid escalation.

**Feedback loop.** Mark a decision wrong; after enough marks for the same signature, ZeroV4 tells you which file to look at. It never edits its own rules, never unbans on its own, never weakens anything by itself. The decision is yours.

**SOC panel.** Live stats, blocked list, reputation, whitelist, event log, suggestions. Password-protected, hash in `.env`.

---

## Configuration

```python
from zerov4 import make_config, arx

cfg = make_config(
    "balanced",
    port=8080,
    trusted_proxies=("10.0.0.0/8",),   # required behind nginx/Cloudflare
    redis_url="redis://localhost:6379",
    geoip_db="GeoLite2-Country.mmdb",
)
arx.run(app, cfg=cfg)
```

Or `zerov4.toml`:

```toml
[zerov4]
level = "balanced"
port = 5000
storage = "auto"
panel_enabled = true
trusted_proxies = ["10.0.0.0/8"]
```

Or the environment: `ZEROV4_PORT=8080 ZEROV4_LEVEL=paranoid`.

### Behind a proxy

**Set `trusted_proxies` if anything sits in front of your app.** Forwarding headers are only honoured when the direct peer is on that list. Without it, an attacker sends `X-Real-IP: <your customer>` and gets your customer banned while staying clean. With no proxy configured, `REMOTE_ADDR` is used and headers are ignored.

### Hooks

ZeroV4 does not touch your machine's network, kill processes, or click anything. If you want that, wire it up:

```python
def on_coordinated(ips, reason):
    print(f"{len(ips)} attackers: {reason}")
    # your own response here

cfg = make_config("balanced",
                  on_coordinated_attack=on_coordinated,
                  on_ban=lambda ip, attack, entry: notify(ip),
                  on_threat_cleared=lambda: print("clear"))
```

---

## Custom signatures

A signature is a module with a `NAME` and either `KEYWORDS`, a `detect()` function, or both.

```python
# signatures/my_rule.py
NAME     = "Admin Panel Probing"
KEYWORDS = ["/wp-login", "/administrator"]

def detect(line, parsed, now, counters):
    ip = parsed["ip"]
    if "admin" in line.lower():
        counters["admin_probe"][ip].append(now)
        counters["admin_probe"][ip] = [
            t for t in counters["admin_probe"][ip] if now - t <= 60
        ]
        if len(counters["admin_probe"][ip]) >= 5:
            return f"{len(counters['admin_probe'][ip])} admin probes in 60s"
    return None
```

Point at your own directory:

```python
cfg = make_config("balanced", signatures_dir="./signatures")
```

A signature that raises is logged and quarantined for the run, not allowed to take the request down with it.

Loading a signature directory imports and executes those `.py` files. Don't point it at a directory other people can write to.

---

## Panel

```bash
zerov4 init     # asks for a panel password
```

The password is never written to disk — only a PBKDF2-SHA256 hash, into `.env` at mode 0600. Add `.env` to your `.gitignore`; ZeroV4 warns you if you haven't.

Panel lives at `/panel` by default. It fails closed: no password set means no logins, not an open door.

---

## API

```python
from zerov4 import arx

arx.run(app)                    # serve
arx.wrap(app)                   # wrap only (gunicorn)
arx.stop()

arx.status()                    # stats dict
arx.blocked()                   # list of blocked IPs
arx.reputation()                # learned bad IPs
arx.attacks(20)                 # recent detections
arx.is_blocked("1.2.3.4")

arx.unblock("1.2.3.4")
arx.ban("1.2.3.4")
arx.whitelist("10.0.0.5")       # never blocked
arx.whitelist()                 # list

arx.events()                    # firewall decisions, with ids
arx.wrong(event_id)             # mark a false positive
arx.suggestions()               # signatures worth reviewing

arx.set_panel_password()
arx.reload_signatures()
```

---

## Notes

**Locked out?** A whitelisted address is never blocked by any rule. Add yours before tightening anything: `zerov4 whitelist YOUR.IP`. Whitelisting an already-banned IP clears the ban.

**Storage.** Redis if reachable, JSON files otherwise, both automatic. `storage="redis"` fails loudly instead of silently degrading — a security system whose bans quietly stop persisting is worse than one that tells you.

**Multiple workers.** Under `gunicorn -w 4`, use Redis. With JSON, each worker keeps its own view and rate limits count per worker.

**Failure mode.** If ZeroV4 itself crashes mid-inspection, the request is allowed and the error is logged. A bug here should cost you protection, not uptime.

**Threads.** A background agent handles pattern analysis and cleanup, and a saver persists state periodically. Both are daemon threads that stop cleanly.

---

## Status

Beta. Test it against your own traffic before putting it in front of anything that matters — start at `permissive` or `balanced`, watch what gets blocked, then tighten. A misconfigured WAF either bans your users or waves the attacker through, and only your traffic can tell you which one you have.

---

## Credits

Derived from Autonomous Agent Zero-V4 by Barış ([BRSX-Labs](https://github.com/brsx-labs)).

Apache 2.0.
