Download runtime/python/dibaguard/shield/rules.py from Dibachain/DibaGuard: direct link, hf CLI and curl.
- Browser
- Download file 12.5 kB
-
https://huggingface.co/Dibachain/DibaGuard/resolve/main/runtime/python/dibaguard/shield/rules.py
- Command line
-
hf download hf://Dibachain/DibaGuard/runtime/python/dibaguard/shield/rules.py
-
curl -L -o rules.py https://huggingface.co/Dibachain/DibaGuard/resolve/main/runtime/python/dibaguard/shield/rules.py
12.5 kB
| """Rules: the operator's own protection, written in plain language (Persian or English), active from the first | |
| second - so there is no "unprotected while it learns" window. | |
| A rule is interpreted into a precise, deterministic predicate over what the engine already measures (no model, no | |
| latency). Interpretation is done by a tiny built-in parser for the common shapes, and - when an LLM is wired in - | |
| by the LLM, which must return the same JSON schema; either way the layer shows its interpretation back so the | |
| operator can confirm it before it acts. | |
| Rule JSON: | |
| {"id": "...", "text": "<original>", "scope": "flow|http|source", "when": {<conditions>}, "action": "block|rate_limit|challenge|monitor|allow", | |
| "family": "<name>", "ttl_s": 1800, "note": "<how it was interpreted>"} | |
| Conditions understood by the parser and the engine (all optional, AND-ed): | |
| port / ports (list), proto (tcp|udp|icmp), path_contains, ua_contains, country (if a geo hook is set), method, | |
| flows_per_min_over, new_ports_over, noreply_over, status, request_rate_over, service (name), src_in (cidr list) | |
| """ | |
| from __future__ import annotations | |
| import ipaddress | |
| import json | |
| import re | |
| ACTIONS = ("block", "rate_limit", "challenge", "monitor", "allow") | |
| # a tiny bilingual lexicon so the built-in parser handles the common operator rules without an LLM | |
| _ACT = {"block": "block", "\u0645\u0633\u062f\u0648\u062f": "block", "\u0628\u0628\u0646\u062f": "block", "deny": "block", "drop": "block", "\u0631\u062f": "block", | |
| "rate": "rate_limit", "\u0645\u062d\u062f\u0648\u062f": "rate_limit", "throttle": "rate_limit", "\u0646\u0631\u062e": "rate_limit", | |
| "challenge": "challenge", "\u0686\u0627\u0644\u0634": "challenge", "captcha": "challenge", "\u06a9\u067e\u0686\u0627": "challenge", | |
| "allow": "allow", "\u0627\u062c\u0627\u0632\u0647": "allow", "\u0645\u062c\u0627\u0632": "allow", "whitelist": "allow", "\u0633\u0641\u06cc\u062f": "allow", | |
| "monitor": "monitor", "\u0641\u0642\u0637 \u06af\u0632\u0627\u0631\u0634": "monitor", "\u067e\u0627\u06cc\u0634": "monitor", "\u06af\u0632\u0627\u0631\u0634": "monitor"} | |
| _PORT = re.compile(r"(?:port|\u067e\u0648\u0631\u062a)\s*#?\s*(\d[\d,\s]*)", re.I) | |
| _FILLER = r"(?:containing|contains|contain|with|=|:|\u062d\u0627\u0648\u06cc|\u0634\u0627\u0645\u0644|\u06a9\u0647|\u0628\u0631\u0627\u0628\u0631|is|to|\u0628\u0631\u0627\u06cc)\s+" | |
| _PATH = re.compile(r"(?:path|url|\u0645\u0633\u06cc\u0631|\u0622\u062f\u0631\u0633)\s*(?:" + _FILLER + r")?([^\s,\u060c]+)", re.I) | |
| _BARE_PATH = re.compile(r"(?<![\w])(/[\w./\-]+)") | |
| _UA = re.compile(r"(?:user-?agent|ua|\u0639\u0627\u0645\u0644|\u0645\u0631\u0648\u0631\u06af\u0631)\s*(?:" + _FILLER + r")?([\w./\-]+)", re.I) | |
| _RATE = re.compile(r"(?:more than|over|\u0628\u06cc\u0634 \u0627\u0632|\u0628\u06cc\u0634\u062a\u0631 \u0627\u0632)\s*(\d+)\s*(?:req|request|\u062f\u0631\u062e\u0648\u0627\u0633\u062a|/|per|\u062f\u0631|flows?|connections?|\u0627\u062a\u0635\u0627\u0644)", re.I) | |
| _PORTS_N = re.compile(r"(?:more than|over|\u0628\u06cc\u0634 \u0627\u0632|\u0628\u06cc\u0634\u062a\u0631 \u0627\u0632)\s*(\d+)\s*(?:ports?|\u067e\u0648\u0631\u062a)", re.I) | |
| _ATTEMPTS = re.compile(r"(?:more than|over|\u0628\u06cc\u0634 \u0627\u0632|\u0628\u06cc\u0634\u062a\u0631 \u0627\u0632)\s*(\d+)\s*(?:attempts?|tries|logins?|\u062a\u0644\u0627\u0634|\u0628\u0627\u0631|\u0648\u0631\u0648\u062f)", re.I) | |
| _CIDR = re.compile(r"\b(\d{1,3}(?:\.\d{1,3}){3}(?:/\d{1,2})?)\b") | |
| _SERVICES = {"ssh": 22, "rdp": 3389, "telnet": 23, "ftp": 21, "smb": 445, "redis": 6379, "mongodb": 27017, | |
| "elasticsearch": 9200, "mysql": 3306, "postgres": 5432, "vnc": 5900, "http": 80, "https": 443} | |
| _SCAN_WORDS = ("scan", "\u0627\u0633\u06a9\u0646", "port scan", "\u0627\u0633\u06a9\u0646 \u067e\u0648\u0631\u062a") | |
| _BRUTE_WORDS = ("brute", "brute force", "\u0628\u0631\u0648\u062a", "\u0641\u0648\u0631\u0633", "\u0631\u0645\u0632", "password") | |
| class Rule: | |
| def __init__(self, d: dict): | |
| self.id = d.get("id", "") | |
| self.text = d.get("text", "") | |
| self.scope = d.get("scope", "source") | |
| self.when = d.get("when", {}) | |
| self.action = d.get("action", "monitor") | |
| self.family = d.get("family", "policy_rule") | |
| self.ttl_s = float(d.get("ttl_s", 1800)) | |
| self.note = d.get("note", "") | |
| if self.action not in ACTIONS: | |
| raise ValueError(f"rule action must be one of {ACTIONS}: {self.action!r}") | |
| def as_dict(self) -> dict: | |
| return {"id": self.id, "text": self.text, "scope": self.scope, "when": self.when, "action": self.action, | |
| "family": self.family, "ttl_s": self.ttl_s, "note": self.note} | |
| # -- evaluation (deterministic; no model). ctx is a dict the layer fills per flow / request. | |
| def matches(self, ctx: dict) -> bool: | |
| w = self.when | |
| if "ports" in w and int(ctx.get("port", -1)) not in w["ports"]: | |
| return False | |
| if "proto" in w and str(ctx.get("proto", "")).lower() != w["proto"]: | |
| return False | |
| if "path_contains" in w and w["path_contains"] not in str(ctx.get("path", "")).lower(): | |
| return False | |
| if "ua_contains" in w and w["ua_contains"] not in str(ctx.get("ua", "")).lower(): | |
| return False | |
| if "method" in w and str(ctx.get("method", "")).lower() != w["method"]: | |
| return False | |
| if "status" in w and int(ctx.get("status", 0)) != int(w["status"]): | |
| return False | |
| if "src_in" in w: | |
| try: | |
| a = ipaddress.ip_address(ctx.get("src", "")) | |
| if not any(a in ipaddress.ip_network(c, strict=False) for c in w["src_in"]): | |
| return False | |
| except ValueError: | |
| return False | |
| if "flows_per_min_over" in w and ctx.get("flows_per_min", 0) <= w["flows_per_min_over"]: | |
| return False | |
| if "request_rate_over" in w and ctx.get("flows_per_min", 0) <= w["request_rate_over"]: | |
| return False | |
| if "new_ports_over" in w and ctx.get("new_ports", 0) <= w["new_ports_over"]: | |
| return False | |
| if "noreply_over" in w and ctx.get("noreply", 0) <= w["noreply_over"]: | |
| return False | |
| if "repeat_over" in w and ctx.get("repeat", 0) <= w["repeat_over"]: | |
| return False | |
| if "family" in w and str(ctx.get("family", "")) != w["family"]: | |
| return False | |
| return bool(w) # an empty rule never matches (prevents an accidental block-all) | |
| def parse(text: str) -> dict: | |
| """built-in interpreter: a plain-language rule -> rule dict (best effort; the LLM path returns the same shape). | |
| Examples it understands: | |
| '\u0645\u0633\u062f\u0648\u062f \u06a9\u0646 \u0647\u0631 \u06a9\u0633\u06cc \u06a9\u0647 \u067e\u0648\u0631\u062a \u06f2\u06f2 \u0631\u0627 brute force \u0645\u06cc\u200c\u06a9\u0646\u062f' -> block, service ssh, scan/brute behaviour | |
| 'block any source scanning more than 50 ports per minute' -> block, new_ports behaviour | |
| 'allow 10.0.0.0/8' -> allow, src_in | |
| 'rate limit requests to /api over 100 per minute' -> rate_limit, path /api, request_rate | |
| 'challenge user-agent containing bot' -> challenge, ua bot | |
| """ | |
| low = _norm(text) | |
| action = next((a for k, a in _ACT.items() if k in low), "monitor") | |
| when: dict = {} | |
| scope = "source" | |
| fam = "policy_rule" | |
| m = _PORT.search(text) | |
| if m: | |
| when["ports"] = [int(x) for x in re.findall(r"\d+", m.group(1))] | |
| for svc, port in _SERVICES.items(): | |
| if re.search(rf"\b{svc}\b", low): | |
| when.setdefault("ports", []).append(port) | |
| when["ports"] = sorted(set(when["ports"])) | |
| mp = _PATH.search(text) or _BARE_PATH.search(text) | |
| if mp and ("/" in mp.group(1) or "." in mp.group(1)): | |
| when["path_contains"] = mp.group(1).lower().strip("'\"") | |
| scope = "http" | |
| mu = _UA.search(text) | |
| if mu: | |
| when["ua_contains"] = mu.group(1).lower().strip("'\"") | |
| scope = "http" | |
| mc = _CIDR.search(text) | |
| if mc: | |
| when["src_in"] = [mc.group(1)] | |
| mr = _RATE.search(low) | |
| if mr: | |
| key = "request_rate_over" if scope == "http" else "flows_per_min_over" | |
| when[key] = int(mr.group(1)) | |
| mp2 = _PORTS_N.search(low) | |
| if any(w in low for w in _SCAN_WORDS) or mp2: | |
| when["new_ports_over"] = int(mp2.group(1)) if mp2 else when.get("new_ports_over", 20) | |
| fam = "portscan" | |
| if any(w in low for w in _BRUTE_WORDS): | |
| # brute force = the same service again and again (an SSH guess completes its handshake: replies are normal) | |
| when["repeat_over"] = when.get("repeat_over", 0.6) | |
| mn = _ATTEMPTS.search(low) | |
| when["flows_per_min_over"] = int(mn.group(1)) if mn else when.get("flows_per_min_over", 10) | |
| fam = "bruteforce" | |
| note = _describe(action, when, scope) | |
| return {"text": text, "scope": scope, "when": when, "action": action, "family": fam, "note": note} | |
| def _norm(s: str) -> str: | |
| s = s.lower() | |
| for a, b in zip("\u06f0\u06f1\u06f2\u06f3\u06f4\u06f5\u06f6\u06f7\u06f8\u06f9\u0660\u0661\u0662\u0663\u0664\u0665\u0666\u0667\u0668\u0669", "01234567890123456789"): | |
| s = s.replace(a, b) | |
| return s | |
| def _describe(action, when, scope) -> str: | |
| if not when: | |
| return "could not interpret any condition - this rule will be inactive until edited" | |
| parts = [] | |
| if "ports" in when: | |
| parts.append(f"destination port in {when['ports']}") | |
| if "path_contains" in when: | |
| parts.append(f"path contains '{when['path_contains']}'") | |
| if "ua_contains" in when: | |
| parts.append(f"user-agent contains '{when['ua_contains']}'") | |
| if "src_in" in when: | |
| parts.append(f"source in {when['src_in']}") | |
| if "flows_per_min_over" in when: | |
| parts.append(f"more than {when['flows_per_min_over']} flows/min from the source") | |
| if "request_rate_over" in when: | |
| parts.append(f"more than {when['request_rate_over']} requests/min from the source") | |
| if "new_ports_over" in when: | |
| parts.append(f"more than {when['new_ports_over']} new ports touched (scanning)") | |
| if "noreply_over" in when: | |
| parts.append(f"over {when['noreply_over']:.0%} of flows unanswered (failed connects)") | |
| if "repeat_over" in when: | |
| parts.append(f"over {when['repeat_over']:.0%} of flows to the same service again (repeated attempts)") | |
| return f"{action.upper()} a {scope} when " + " and ".join(parts) | |
| LLM_SCHEMA_PROMPT = """You translate one network/web security rule written by an operator (Persian or English) into | |
| a JSON object, and nothing else. Schema: | |
| {"scope":"flow|http|source","when":{...},"action":"block|rate_limit|challenge|monitor|allow","family":"<name>","note":"<one sentence back-translation of what you understood>"} | |
| Allowed keys in "when": ports (list of ints), proto ("tcp"|"udp"|"icmp"), path_contains (str), ua_contains (str), | |
| method (str), status (int), src_in (list of CIDR), flows_per_min_over (num), request_rate_over (num), | |
| new_ports_over (num per minute), noreply_over (0..1), repeat_over (0..1, share of flows to an already-used host:port: brute force), family (str). Use only conditions the text states. Never invent a | |
| block-all. Return ONLY the JSON.""" | |
| class RuleSet: | |
| def __init__(self, rules: list | None = None): | |
| self.rules: list[Rule] = [Rule(r) if isinstance(r, dict) else r for r in (rules or [])] | |
| def add(self, text: str, interpret, idx: int) -> Rule: | |
| d = interpret(text) if interpret else parse(text) | |
| d["id"] = d.get("id") or f"R{idx:03d}" | |
| taken = {r.id for r in self.rules} | |
| if d["id"] in taken: # ids stay unique after a removal | |
| d["id"] = f"R{1 + max((int(x[1:]) for x in taken if re.fullmatch(r'R\d+', x)), default=0):03d}" | |
| r = Rule(d) | |
| self.rules.append(r) | |
| return r | |
| def remove(self, rule_id: str) -> Rule | None: | |
| """take one rule out (by id); returns it, or None when there is no such rule""" | |
| for i, r in enumerate(self.rules): | |
| if r.id == rule_id: | |
| return self.rules.pop(i) | |
| return None | |
| def decide(self, ctx: dict) -> Rule | None: | |
| """first matching rule wins (operator order); 'allow' short-circuits to no action""" | |
| for r in self.rules: | |
| if r.scope in (ctx.get("scope"), "source") and r.matches(ctx): | |
| return r | |
| return None | |