Skip to main content

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.

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

pip install zerov4

Optional extras:

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:

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:

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:

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:

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:

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:

from myapp import app       # your file, in this folder
arx.run(app)
arx.run("myapp:app")        # module:attribute, same thing

Under gunicorn

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

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

application = arx.wrap(app)
gunicorn wsgi:application -w 4

As middleware

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

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 no (opt-in)
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 treats ordinary traffic as hostile: every 404 counts toward a ban, keyword matching goes fuzzy, and a session is bound to its IP — so a phone moving from WiFi to cellular reads as session theft. It is for a target you control, not a public site. ZeroV4 asks you to confirm before enabling it.

Non-JSON bodies are never an attack, paranoid included. An HTML form posts application/x-www-form-urlencoded and a file upload posts multipart/form-data; treating either as hostile bans the visitors of any site that has a form. If you run a JSON-only API, where a non-JSON body really is anomalous, ask for it:

cfg = make_config("paranoid", non_json_body_is_attack=True)

Even then, a request that declares a form content type is exempt — the rule catches a body that lies about being JSON, not one that never claimed to be.


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

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:

[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:

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.

# 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:

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

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.


Badge

Optional. Two lines, and it's yours to place:

from zerov4 import arx
from myapp import app

arx.badge(app)        # registers the template helper
arx.run(app)
<body>
  ...
  {{ zerov4_badge() }}
</body>

A small grey pill with white text, in the corner, scrolling with the page.

{{ zerov4_badge("bottom-right") }}   <!-- top-right (default), top-left,
                                          bottom-right, bottom-left -->
{{ zerov4_badge(dark=False) }}       <!-- lighter pill, for a pale page -->
{{ zerov4_badge(bg="#1e293b", fg="#e2e8f0") }}   <!-- your own colours -->

Not using Flask? arx.badge() with no argument returns the markup; paste it wherever you like.

ZeroV4 does not inject the badge into your responses. Rewriting your HTML on the way out breaks gzip, breaks streaming, breaks Content-Length — and when it breaks, it breaks your site. Where the badge goes is your call.

The badge doesn't name your protection level, and it shouldn't: the signatures are readable on PyPI, so which rules you're running is the one detail worth keeping to yourself.


API

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()

arx.badge(app)                  # register {{ zerov4_badge() }}
arx.badge()                     # just the markup

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).

Apache 2.0.

Release files for zerov4 0.2.9

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for zerov4 0.2.9
File Size Uploaded
zerov4-0.2.9.tar.gz 236.6 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for zerov4 0.2.9
File Interpreter ABI Platform
zerov4-0.2.9-py3-none-any.whl Python 3 none any Details

Total release size: 780.9 kB

Release files / zerov4-0.2.9.tar.gz

Download URL zerov4-0.2.9.tar.gz
Size 236.6 kB
Tags Source
SHA-256 checksum
How to use checksums
8f7c6454068a6d4e15e05258265163437e920ea0d7b6d4ace0f164a78835a4e0
BLAKE2b-256 checksum
How to use checksums
5694bca9820347f19ef21b6f38e39a149ac6a3a7d2d4280a6e94168ecb397b79
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/6.2.0 CPython/3.10.0

Release files / zerov4-0.2.9-py3-none-any.whl

Download URL zerov4-0.2.9-py3-none-any.whl
Size 544.3 kB
Tags Python 3
SHA-256 checksum
How to use checksums
436b9f2b87c6223b674eb099a9865869550efacc941ff7d0661f12226a1dd521
BLAKE2b-256 checksum
How to use checksums
3c04f3d2a64cca30c7798cef5f319e71a9e22917fe431b03134123b0b68b0f52
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/6.2.0 CPython/3.10.0

Release history Release notifications | RSS feed

0.3.0

2 release files

This release

0.2.9 This release

2 release files

0.2.8

2 release files

0.2.7

2 release files

0.2.6

2 release files

0.2.5

2 release files

0.2.4

2 release files

0.2.3

2 release files

0.2.2

2 release files

0.2.1

2 release files

0.2.0

2 release files

0.1.9

2 release files

0.1.8

2 release files

0.1.7

2 release files

0.1.6

2 release files

0.1.5

2 release files

0.1.4

2 release files

0.1.3

2 release files

0.1.2

2 release files

0.1.1

2 release files

0.1.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page