"""CYPHER V12 M5 — Defensive rules validator (YARA + Sigma + Suricata). Validates security rules generated/analyzed by CYPHER, annotates output. Post-processing layer: detect blocks via regex, compile/parse, append [VALID] or [SYNTAX_ERROR: ...] marker. """ from __future__ import annotations import logging import re from typing import Any import yaml try: import yara # noqa: F401 _HAS_YARA = True except ImportError: _HAS_YARA = False logger = logging.getLogger(__name__) # Block detection patterns _YARA_RULE_RE = re.compile( r"(rule\s+[A-Za-z_][A-Za-z0-9_]*\s*(?::[^\n{]+)?\s*\{[^}]*\})", re.DOTALL, ) _YARA_STRINGS_RE = re.compile(r'\$[A-Za-z0-9_]+\s*=\s*"([^"\\]*(?:\\.[^"\\]*)*)"') _YARA_NAME_RE = re.compile(r"rule\s+([A-Za-z_][A-Za-z0-9_]*)") # Sigma: detect YAML blocks with 'title' + 'detection' keys _SIGMA_BLOCK_RE = re.compile( r"(---\s*\n.*?(?=\n---|\Z)|(? dict: rule_name = None m = _YARA_NAME_RE.search(rule_str) if m: rule_name = m.group(1) if not _HAS_YARA: return { "valid": None, "error": "yara-python not installed (cannot verify)", "rule_name": rule_name, } try: yara.compile(source=rule_str) return {"valid": True, "error": None, "rule_name": rule_name} except yara.SyntaxError as e: return {"valid": False, "error": str(e), "rule_name": rule_name} except yara.Error as e: return {"valid": False, "error": f"YARA error: {e}", "rule_name": rule_name} def extract_strings(self, rule_str: str) -> list[str]: return [m for m in _YARA_STRINGS_RE.findall(rule_str)] class SigmaValidator: """Validate Sigma rules: YAML parse + required fields check.""" REQUIRED_FIELDS = ("title", "logsource", "detection") def validate(self, yaml_str: str) -> dict: try: doc = yaml.safe_load(yaml_str) except yaml.YAMLError as e: return {"valid": False, "error": f"YAML parse: {e}", "title": None, "level": None} if not isinstance(doc, dict): return {"valid": False, "error": "Not a mapping", "title": None, "level": None} missing = [k for k in self.REQUIRED_FIELDS if k not in doc] if missing: return { "valid": False, "error": f"Missing required fields: {missing}", "title": doc.get("title"), "level": doc.get("level"), } det = doc.get("detection") if not isinstance(det, dict): return {"valid": False, "error": "detection must be a mapping", "title": doc.get("title"), "level": doc.get("level")} if "condition" not in det: return {"valid": False, "error": "detection.condition missing", "title": doc.get("title"), "level": doc.get("level")} return { "valid": True, "error": None, "title": doc.get("title"), "level": doc.get("level"), } def extract_logsource(self, yaml_str: str) -> dict: try: doc = yaml.safe_load(yaml_str) if isinstance(doc, dict): ls = doc.get("logsource", {}) if isinstance(ls, dict): return ls except yaml.YAMLError: pass return {} class SuricataValidator: """Minimal Suricata/Snort rule validator. Verifies: - Starts with action (alert|drop|reject|pass) - Has protocol + IP/port spec + options in (...) - sid present """ ACTIONS = ("alert", "drop", "reject", "pass") PROTOCOLS = ("tcp", "udp", "icmp", "ip", "http", "tls", "dns", "smb", "ftp", "smtp") def validate(self, rule_str: str) -> dict: rule_str = rule_str.strip() if not rule_str: return {"valid": False, "error": "empty rule", "sid": None, "msg": None} parts = rule_str.split(None, 1) action = parts[0].lower() if action not in self.ACTIONS: return {"valid": False, "error": f"unknown action: {action}", "sid": None, "msg": None} if "(" not in rule_str or ")" not in rule_str: return {"valid": False, "error": "missing options ( ... )", "sid": None, "msg": None} # Check protocol present proto_found = any(f" {p} " in f" {rule_str.lower()} " for p in self.PROTOCOLS) if not proto_found: return {"valid": False, "error": "no recognized protocol", "sid": None, "msg": None} sid_m = _SURICATA_SID_RE.search(rule_str) msg_m = _SURICATA_MSG_RE.search(rule_str) if not sid_m: return {"valid": False, "error": "sid: missing", "sid": None, "msg": msg_m.group(1) if msg_m else None} return { "valid": True, "error": None, "sid": int(sid_m.group(1)), "msg": msg_m.group(1) if msg_m else None, } def extract_classtype(self, rule_str: str) -> str | None: m = _SURICATA_CLASSTYPE_RE.search(rule_str) return m.group(1) if m else None def extract_rules_from_text(text: str) -> dict[str, list[str]]: """Detect and extract YARA, Sigma, Suricata rule blocks from arbitrary text.""" yara_blocks = _YARA_RULE_RE.findall(text) # Sigma: blocks starting with --- or 'title:' followed by 'detection:' nearby sigma_blocks: list[str] = [] for m in _SIGMA_BLOCK_RE.finditer(text): block = m.group(0).strip() if "detection" in block and "title" in block and "logsource" in block: sigma_blocks.append(block) # Suricata: lines starting with action verb suricata_blocks: list[str] = [] for line in text.split("\n"): line = line.strip() if line and line.split(None, 1)[0].lower() in SuricataValidator.ACTIONS: if "(" in line and ")" in line: suricata_blocks.append(line) return { "yara": yara_blocks, "sigma": sigma_blocks, "suricata": suricata_blocks, } def annotate_response(response: str) -> str: """Detect rule blocks in CYPHER output, validate each, append marker.""" blocks = extract_rules_from_text(response) yv = YaraValidator() sv = SigmaValidator() sur = SuricataValidator() annotations: list[str] = [response] for b in blocks["yara"]: r = yv.validate(b) if r["valid"] is True: annotations.append(f"\n[YARA VALID: {r['rule_name'] or 'anonymous'}]") elif r["valid"] is False: annotations.append(f"\n[YARA SYNTAX_ERROR: {r['error']}]") else: annotations.append(f"\n[YARA UNVERIFIED: {r['error']}]") for b in blocks["sigma"]: r = sv.validate(b) if r["valid"]: annotations.append(f"\n[SIGMA VALID: {r['title']} level={r['level']}]") else: annotations.append(f"\n[SIGMA SYNTAX_ERROR: {r['error']}]") for b in blocks["suricata"]: r = sur.validate(b) if r["valid"]: annotations.append(f"\n[SURICATA VALID: sid={r['sid']} msg={r['msg']}]") else: annotations.append(f"\n[SURICATA SYNTAX_ERROR: {r['error']}]") return "".join(annotations) __all__ = [ "YaraValidator", "SigmaValidator", "SuricataValidator", "extract_rules_from_text", "annotate_response", ] if __name__ == "__main__": logging.basicConfig(level=logging.INFO) print("=== M5 cypher_rule_validator SMOKE TEST ===") # 1. YARA valid_yara = ''' rule Test_Mimikatz { meta: author = "test" strings: $a = "sekurlsa::logonpasswords" $b = "mimikatz" condition: any of them } ''' invalid_yara = "rule Bad { strings: $x = invalid_no_quotes condition: $x }" yv = YaraValidator() r1 = yv.validate(valid_yara) r2 = yv.validate(invalid_yara) print(f"YARA valid: {r1}") print(f"YARA invalid: {r2}") strings = yv.extract_strings(valid_yara) print(f"YARA strings extracted: {strings}") # 2. Sigma valid_sigma = """title: Suspicious PowerShell Download logsource: product: windows service: powershell detection: selection: EventID: 4104 ScriptBlockText|contains: - 'DownloadString' - 'IEX' condition: selection level: high """ invalid_sigma = "title: Bad\nfoo: bar\n" sv = SigmaValidator() r3 = sv.validate(valid_sigma) r4 = sv.validate(invalid_sigma) print(f"Sigma valid: {r3}") print(f"Sigma invalid: {r4}") # 3. Suricata valid_suri = 'alert tcp $HOME_NET any -> $EXTERNAL_NET 443 (msg:"ET MALWARE Suspicious TLS"; classtype:trojan-activity; sid:2000001; rev:1;)' invalid_suri = "unknown tcp $HOME_NET -> any 80" sur = SuricataValidator() r5 = sur.validate(valid_suri) r6 = sur.validate(invalid_suri) print(f"Suricata valid: {r5}") print(f"Suricata invalid: {r6}") # 4. extract_rules_from_text + annotate_response response = "Here are some defensive rules:\n" + valid_yara + "\nAlso this Sigma rule:\n" + valid_sigma + "\nAnd a Suricata rule:\n" + valid_suri extracted = extract_rules_from_text(response) print(f"Extracted: yara={len(extracted['yara'])} sigma={len(extracted['sigma'])} suricata={len(extracted['suricata'])}") annotated = annotate_response(response) print(f"Annotated tail (last 300 chars):\n{annotated[-300:]}") print("=== SMOKE PASS ===")