diff --git a/.github/workflows/test-and-build.yml b/.github/workflows/test-and-build.yml index db26255..c2aa978 100644 --- a/.github/workflows/test-and-build.yml +++ b/.github/workflows/test-and-build.yml @@ -37,6 +37,13 @@ jobs: run: | mkdir -p data/openclaw/raw mkdir -p data/openclaw/replay/labeled + # Materialize the benchmark corpus expected by regression tests. + if [ -f "data/fixtures/events.json" ]; then + cp data/fixtures/events.json data/events.json + fi + if [ -f "data/fixtures/events_unlabeled.json" ]; then + cp data/fixtures/events_unlabeled.json data/events_unlabeled.json + fi # Copy fixture data if [ -f "data/fixtures/openclaw/sample_audit.jsonl" ]; then cp data/fixtures/openclaw/sample_audit.jsonl data/openclaw/raw/audit.jsonl @@ -46,34 +53,36 @@ jobs: fi # Create mock schema file for tests (at parent directory level) mkdir -p ../secopsai-dashboard/supabase_migrations - cat > ../secopsai-dashboard/supabase_migrations/2026-03-28_findings.sql << 'SCHEMA_EOF' -create table if not exists public.findings ( - id uuid primary key default gen_random_uuid(), - external_finding_id text not null, - title text not null, - summary text, - severity text, - severity_score int, - status text, - disposition text, - confidence float, - source text, - source_name text, - detector text, - fingerprint text, - dedupe_key text, - detected_at timestamptz, - first_seen_at timestamptz, - last_seen_at timestamptz, - rule_id text, - rule_name text, - mitre text, - event_count int, - event_ids jsonb, - recommended_actions jsonb, - raw_payload jsonb -); -SCHEMA_EOF + cat > ../secopsai-dashboard/supabase_migrations/2026-03-28_findings.sql <<'SCHEMA_EOF' + create table if not exists public.findings ( + id uuid primary key default gen_random_uuid(), + external_finding_id text not null, + title text not null, + summary text, + severity text, + severity_score int, + status text, + disposition text, + confidence text, + source text, + source_platform text, + correlation_type text, + detection_layer text, + detected_at timestamptz, + first_seen_at timestamptz, + last_seen_at timestamptz, + rule_id text, + rule_name text, + mitre text, + mitre_ids text[], + event_count int, + event_ids jsonb, + risk_tags text[], + recommended_actions jsonb, + raw_payload jsonb, + metadata jsonb + ); + SCHEMA_EOF - name: Lint with flake8 run: | diff --git a/correlation.py b/correlation.py index 302a1e7..6f30971 100644 --- a/correlation.py +++ b/correlation.py @@ -629,12 +629,25 @@ def correlate_by_file_hash(findings: List[Dict]) -> List[Dict]: return correlations -def run_correlation(findings: List[Dict]) -> Dict[str, Any]: - """Run all correlation rules.""" +def run_correlation( + findings: List[Dict], + time_window_minutes: int = 60, +) -> Dict[str, Any]: + """Run all correlation rules. + + ``time_window_minutes`` is accepted for backward compatibility with older + CLI callers that pass a configurable window. + """ + if not isinstance(time_window_minutes, int): + try: + time_window_minutes = int(time_window_minutes) + except (TypeError, ValueError): + time_window_minutes = 60 + results = { - "cross_platform_ip": correlate_by_ip(findings), - "cross_platform_user": correlate_by_user(findings), - "time_cluster": correlate_by_time(findings), + "cross_platform_ip": correlate_by_ip(findings, time_window_minutes=time_window_minutes), + "cross_platform_user": correlate_by_user(findings, time_window_minutes=time_window_minutes), + "time_cluster": correlate_by_time(findings, time_window_minutes=time_window_minutes), "cross_platform_file": correlate_by_file_hash(findings), "total_correlations": 0 } diff --git a/detect.py b/detect.py index 1bb2731..4784c55 100644 --- a/detect.py +++ b/detect.py @@ -11,7 +11,7 @@ import re import math from datetime import datetime, timezone -from typing import List, Dict, Any, Optional, Iterable +from typing import List, Dict, Any, Optional, Iterable, DefaultDict, Callable, cast from collections import Counter, defaultdict import hashlib @@ -55,31 +55,31 @@ "brute_force": { # tune.py (quick grid, 2026-03-17): RAPID_THRESHOLD 6→4, WINDOW 10→5 min # lifted overall F1 from 0.720 → 0.796 by catching more slow/distributed bursts. - "RAPID_THRESHOLD": 4, - "RAPID_WINDOW_MINUTES": 5, + "RAPID_THRESHOLD": 12, + "RAPID_WINDOW_MINUTES": 15, "SLOW_THRESHOLD": 2, - "SLOW_MIN_SPAN_MINUTES": 15, + "SLOW_MIN_SPAN_MINUTES": 60, "COMPROMISE_WINDOW_MINUTES": 20, "SOURCELESS_THRESHOLD": 8, }, "dns_exfiltration": { - "MIN_QUERIES_PER_DOMAIN": 3, - "MIN_LABEL_LENGTH": 10, - "MIN_ENTROPY": 2.5, - "MIN_UNIQUE_LABEL_RATIO": 0.6, + "MIN_QUERIES_PER_DOMAIN": 5, + "MIN_LABEL_LENGTH": 20, + "MIN_ENTROPY": 4.0, + "MIN_UNIQUE_LABEL_RATIO": 0.8, "FALLBACK_LABEL_LENGTH": 20, "FALLBACK_UNIQUE_RATIO": 0.7, }, "c2_beaconing": { "MIN_CONNECTIONS": 3, "MAX_BYTES_OUT": 500, - "MAX_BYTES_IN": 250, + "MAX_BYTES_IN": 400, }, "lateral_movement": { "UNIQUE_DEST_THRESHOLD": 3, - "WINDOW_MINUTES": 20, - "MAX_AVERAGE_GAP_SECONDS": 240, - "MAX_TRANSFER_BYTES": 50000, + "WINDOW_MINUTES": 30, + "MAX_AVERAGE_GAP_SECONDS": 180, + "MAX_TRANSFER_BYTES": 75000, }, } @@ -130,7 +130,7 @@ def shannon_entropy(text: str) -> float: if not text: return 0.0 - counts = defaultdict(int) + counts: DefaultDict[str, int] = defaultdict(int) for char in text: counts[char] += 1 @@ -605,6 +605,69 @@ def _is_macos_event(event: Dict[str, Any]) -> bool: return str(event.get("platform", "")).lower() == "macos" +def _is_windows_event(event: Dict[str, Any]) -> bool: + return str(event.get("platform", "")).lower() == "windows" + + +def _is_linux_event(event: Dict[str, Any]) -> bool: + return str(event.get("platform", "")).lower() == "linux" + + +def _basename(text: str) -> str: + value = (text or "").strip().replace("\\", "/") + return value.rsplit("/", 1)[-1].lower() if value else "" + + +def _process_name(event: Dict[str, Any]) -> str: + actor = event.get("actor") + if isinstance(actor, dict): + for key in ("process", "process_name", "executable_path", "command_line"): + value = actor.get(key) + if isinstance(value, str) and value.strip(): + return _basename(value) + for key in ("process", "process_name", "filepath", "path", "command_line", "command"): + value = event.get(key) + if isinstance(value, str) and value.strip(): + return _basename(value) + return "" + + +def _parent_process_name(event: Dict[str, Any]) -> str: + actor = event.get("actor") + if isinstance(actor, dict): + for key in ("parent_process", "parent_executable"): + value = actor.get(key) + if isinstance(value, str) and value.strip(): + return _basename(value) + value = event.get("parent_process") + return _basename(value) if isinstance(value, str) else "" + + +def _command_line(event: Dict[str, Any]) -> str: + actor = event.get("actor") + if isinstance(actor, dict): + for key in ("command_line", "process", "executable_path"): + value = actor.get(key) + if isinstance(value, str) and value.strip(): + return value + for key in ("command_line", "command", "message", "filepath", "registry_path"): + value = event.get(key) + if isinstance(value, str) and value.strip(): + return value + return "" + + +def _combined_text(event: Dict[str, Any]) -> str: + pieces = [ + _command_line(event), + _event_message(event), + str(event.get("registry_path") or ""), + str(event.get("filepath") or ""), + str(event.get("path") or ""), + ] + return " ".join(piece for piece in pieces if piece) + + def detect_macos_authentication_failures(events: List[Dict]) -> List[str]: """ T1110 — Suspicious macOS authentication failure bursts. @@ -632,6 +695,113 @@ def detect_macos_authentication_failures(events: List[Dict]) -> List[str]: return sorted(detected) +def detect_node_installer_remote_fetch(events: List[Dict]) -> List[str]: + """ + T1195 — Suspicious package-install chain where node/npm/bun spawns native execution + to fetch remote content during dependency installation. + """ + detected = [] + parent_names = {"node", "node.exe", "npm", "npm.cmd", "npx", "bun", "bun.exe"} + child_fetchers = { + "sh", "bash", "dash", "zsh", "ksh", "fish", "curl", "curl.exe", "wget", + "wget.exe", "cmd.exe", "powershell.exe", "cscript.exe", "osascript", + } + url_hint = re.compile(r"(?i)https?://") + install_hint = re.compile(r"(?i)(npm\s+install|bun\s+install|postinstall|node_modules)") + fetch_hint = re.compile(r"(?i)\b(curl|wget|invoke-webrequest|invoke-restmethod)\b") + + for event in events: + event_type = str(event.get("event_type", "")).lower() + if event_type not in {"process", "process_exec", "process_execution", ""} and event.get("sourcetype") != "sysmon": + continue + + parent = _parent_process_name(event) + process = _process_name(event) + combined = _combined_text(event) + if parent not in parent_names: + continue + if process not in child_fetchers: + continue + if (url_hint.search(combined) and fetch_hint.search(combined)) or install_hint.search(combined): + detected.append(event["event_id"]) + return detected + + +def detect_detached_payload_execution(events: List[Dict]) -> List[str]: + """ + T1059 / T1105 — Detect installer-time shells that fetch a payload and detach it. + """ + detached_patterns = re.compile( + r"(?i)(nohup\s+.+\s+&\b|/bin/\w+\s+-c\s+.*\s+&\b|start\s+/min\s+powershell|powershell.*-enc\b|" + r"cscript(?:\.exe)?\s+.*\.vbs\b|osascript\s+.*\.scpt\b|chmod\s+\d+\s+.+&&.+https?://|" + r"invoke-webrequest.*https?://.*\|\s*powershell)" + ) + download_exec = re.compile( + r"(?i)(curl|wget|invoke-webrequest|invoke-restmethod).*(https?://).*(python|bash|sh|zsh|powershell|cmd|osascript|cscript)" + ) + detected = [] + for event in events: + if is_openclaw_event(event): + continue + event_type = str(event.get("event_type", "")).lower() + if event_type not in {"process", "process_exec", "process_execution", "script_execution", ""} and event.get("sourcetype") != "sysmon": + continue + combined = _combined_text(event) + if ("http://" in combined.lower() or "https://" in combined.lower()) and ( + detached_patterns.search(combined) or download_exec.search(combined) + ): + detected.append(event["event_id"]) + return detected + + +def detect_windows_renamed_proxy_persistence(events: List[Dict]) -> List[str]: + """ + T1218 / T1547 — Detect renamed script proxy binaries and associated Run-key persistence. + """ + detected = [] + renamed_proxy = re.compile( + r"(?i)(programdata[/\\]wt\.exe|programdata[/\\].*powershell.*\.exe|powershell.*programdata|" + r"run[/\\]microsoftupdate|currentversion[/\\]run[/\\]microsoftupdate)" + ) + script_fetch = re.compile( + r"(?i)(curl|invoke-webrequest|invoke-restmethod).*(https?://|packages\.npm\.org/product)" + ) + + for event in events: + if not _is_windows_event(event) and event.get("sourcetype") != "sysmon": + continue + combined = _combined_text(event) + process = _process_name(event) + if renamed_proxy.search(combined): + detected.append(event["event_id"]) + continue + if process in {"wt.exe", "powershell.exe", "cmd.exe", "cscript.exe"} and script_fetch.search(combined): + detected.append(event["event_id"]) + return detected + + +def detect_macos_applescript_loader(events: List[Dict]) -> List[str]: + """ + T1059.002 / T1105 — Detect macOS osascript-based loaders that stage and execute remote payloads. + """ + detected = [] + apple_loader = re.compile( + r"(?i)(osascript|\.scpt\b|/library/caches/com\.apple\.[\w.-]+|do shell script|" + r"curl\s+-o\s+/library/caches/com\.apple\.[\w.-]+|/bin/zsh\s+-c\s+.+https?://)" + ) + suspicious_parent = {"node", "npm", "bun", "osascript", "curl"} + + for event in events: + if not _is_macos_event(event): + continue + combined = _combined_text(event) + parent = _parent_process_name(event) + process = _process_name(event) + if apple_loader.search(combined) and (parent in suspicious_parent or process in {"osascript", "zsh", "sh", "curl"}): + detected.append(event["event_id"]) + return detected + + def detect_macos_sudo_misuse(events: List[Dict]) -> List[str]: """ T1548.003 — Suspicious sudo and privilege escalation usage on macOS. @@ -1460,6 +1630,50 @@ def add_and_check(self, metric_name: str, value: float) -> Optional[str]: # ═══ Finding Shaping ══════════════════════════════════════════════════════════ RULE_FINDING_PROFILES: Dict[str, Dict[str, Any]] = { + "RULE-111": { + "title": "Suspicious package install remote fetch", + "severity": "high", + "severity_score": 82, + "summary": "A node/npm/bun install chain spawned native tooling to fetch remote content, which is strongly associated with package postinstall compromise.", + "recommended_actions": [ + "Identify the package name, version, and dependency tree involved in the install sequence.", + "Remove the affected package version, clear local package caches, and inspect developer workstations for follow-on payloads.", + "Block the remote domain or URL if it is not part of an approved package mirror or build workflow.", + ], + }, + "RULE-112": { + "title": "Detached payload execution after remote retrieval", + "severity": "high", + "severity_score": 84, + "summary": "A shell or interpreter fetched remote content and then detached execution, which is a high-signal delivery-stage pattern for supply-chain and loader malware.", + "recommended_actions": [ + "Review the full command line and parent process to confirm whether this was a package install or scripted task.", + "Collect the downloaded payload, execution path, and hashes before cleanup.", + "Inspect the host for persistence and network beacons from the same user or process context.", + ], + }, + "RULE-113": { + "title": "Windows renamed proxy and persistence chain", + "severity": "critical", + "severity_score": 89, + "summary": "A Windows script interpreter or proxy binary executed from an unusual path or wrote suspicious Run-key persistence consistent with staged malware delivery.", + "recommended_actions": [ + "Inspect the referenced ProgramData binary or script for tampering and compare it to a known-good signed binary.", + "Remove unauthorized Run-key persistence and capture the backing script or executable for analysis.", + "Review child processes, network retrievals, and any encoded PowerShell that followed the proxy execution.", + ], + }, + "RULE-214": { + "title": "macOS AppleScript loader chain", + "severity": "high", + "severity_score": 86, + "summary": "A macOS AppleScript or shell loader staged a payload in an Apple-looking cache path or temp script location and executed it.", + "recommended_actions": [ + "Inspect the AppleScript, dropped binary, and cache path for masquerading or ad-hoc signed payloads.", + "Remove unauthorized payloads from /Library/Caches, /tmp, or related staging paths after collecting hashes.", + "Review the full parent-child chain to determine whether a package install or browser-driven script launched the loader.", + ], + }, "RULE-201": { "title": "macOS authentication failures", "severity": "medium", @@ -2063,6 +2277,9 @@ def detect_log4j(events: List[Dict]) -> List[str]: {"id": "RULE-108", "name": "OpenClaw Restart Loop", "mitre": "T1529", "fn": detect_openclaw_restart_loop}, {"id": "RULE-109", "name": "OpenClaw Data Exfiltration", "mitre": "T1048", "fn": detect_openclaw_data_exfiltration}, {"id": "RULE-110", "name": "OpenClaw Malware Presence", "mitre": "T1204", "fn": detect_openclaw_malware_presence}, + {"id": "RULE-111", "name": "Node Installer Remote Fetch", "mitre": "T1195", "fn": detect_node_installer_remote_fetch}, + {"id": "RULE-112", "name": "Detached Payload Execution", "mitre": "T1059", "fn": detect_detached_payload_execution}, + {"id": "RULE-113", "name": "Windows Renamed Proxy Persistence", "mitre": "T1547", "fn": detect_windows_renamed_proxy_persistence}, {"id": "RULE-301", "name": "SQL Injection Detection", "mitre": "T1190", "fn": detect_sql_injection}, {"id": "RULE-302", "name": "Remote Code Execution", "mitre": "T1059", "fn": detect_rce}, {"id": "RULE-303", "name": "Cross-Site Scripting", "mitre": "T1189", "fn": detect_xss}, @@ -2073,6 +2290,7 @@ def detect_log4j(events: List[Dict]) -> List[str]: {"id": "RULE-308", "name": "SSRF Attack", "mitre": "T1189", "fn": detect_ssrf}, {"id": "RULE-309", "name": "NoSQL Injection", "mitre": "T1190", "fn": detect_nosql_injection}, {"id": "RULE-310", "name": "Log4j JNDI Injection", "mitre": "T1190", "fn": detect_log4j}, + {"id": "RULE-214", "name": "macOS AppleScript Loader", "mitre": "T1059.002", "fn": detect_macos_applescript_loader}, ] @@ -2088,17 +2306,19 @@ def run_detection(events: List[Dict]) -> Dict[str, Any]: "total_detections": int, } """ - all_detected = set() - rule_results = {} + all_detected: set[str] = set() + rule_results: Dict[str, List[str]] = {} for rule in DETECTION_RULES: + rule_id = str(rule.get("id", "unknown-rule")) try: - detected_ids = rule["fn"](events) - rule_results[rule["id"]] = detected_ids + rule_fn = cast(Callable[[List[Dict[str, Any]]], List[str]], rule["fn"]) + detected_ids = rule_fn(events) + rule_results[rule_id] = detected_ids all_detected.update(detected_ids) except (KeyError, TypeError, ValueError, ZeroDivisionError, re.error) as e: print(f" ⚠️ Rule {rule['id']} ({rule['name']}) error: {e}") - rule_results[rule["id"]] = [] + rule_results[rule_id] = [] findings = build_detection_findings(events, rule_results) @@ -2111,3 +2331,11 @@ def run_detection(events: List[Dict]) -> Dict[str, Any]: } # ═══ Additional Web Attack Detection Rules ════════════════════════════════════ + + + + + + + + diff --git a/docs/threat-intel.md b/docs/threat-intel.md index f3ab39a..02e4b21 100644 --- a/docs/threat-intel.md +++ b/docs/threat-intel.md @@ -3,10 +3,11 @@ secopsai includes a local-first threat intelligence pipeline that can: 1) Aggregate IOCs from open-source feeds -2) Normalize + de-duplicate + score them -3) Optionally enrich them with lightweight local OSINT (DNS resolution) -4) Match IOCs against your latest OpenClaw replay events -5) Persist any matches as findings in the local SOC store +2) Merge curated high-confidence indicators shipped with SecOpsAI +3) Normalize + de-duplicate + score them +4) Optionally enrich them with lightweight local OSINT (DNS resolution) +5) Match IOCs against your latest OpenClaw replay events +6) Persist any matches as findings in the local SOC store ## Security model (important) @@ -31,6 +32,11 @@ source .venv/bin/activate secopsai intel refresh ``` +This refresh includes: + +- open-source feeds such as URLhaus and ThreatFox +- curated SecOpsAI indicators for notable ecosystem and supply-chain compromise cases + JSON output: ```bash diff --git a/eval/harness/runner.py b/eval/harness/runner.py index ee1eb9c..e68ab36 100644 --- a/eval/harness/runner.py +++ b/eval/harness/runner.py @@ -224,8 +224,11 @@ def evaluate_accuracy(self, scenarios: List[Dict]) -> tuple: scenario_metrics[cat] = ScenarioMetrics(scenario_name=cat) scenario_events = scenario.get("events", [scenario]) - scenario_detected = {e.get("event_id") for e in scenario_events & - e.get("event_id") in detected_ids} + scenario_detected = { + event.get("event_id") + for event in scenario_events + if event.get("event_id") in detected_ids + } cat_matrix = MetricsCalculator.compute_confusion_matrix( detected_ids=scenario_detected, diff --git a/evaluate.py b/evaluate.py index b072edf..8188536 100644 --- a/evaluate.py +++ b/evaluate.py @@ -18,7 +18,7 @@ import argparse import shutil from datetime import datetime -from typing import Dict, Any, List +from typing import Dict, Any, List, Optional # Add project root to path sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) @@ -107,8 +107,8 @@ def compute_per_rule_metrics( # ═══ Reporting ═══════════════════════════════════════════════════════════════ -def print_results(metrics: Dict[str, Any], rule_results: Dict = None, - events: List[Dict] = None, verbose: bool = False): +def print_results(metrics: Dict[str, Any], rule_results: Optional[Dict[str, List[str]]] = None, + events: Optional[List[Dict[str, Any]]] = None, verbose: bool = False): """Print evaluation results in a clear format.""" print(f"\n{'═' * 60}") @@ -148,7 +148,7 @@ def print_results(metrics: Dict[str, Any], rule_results: Dict = None, missed = [e for e in events if e.get("label") == "malicious" and e["event_id"] not in detected_set] if missed: - missed_types = {} + missed_types: dict[str, int] = {} for e in missed: at = e.get("attack_type", "unknown") missed_types[at] = missed_types.get(at, 0) + 1 diff --git a/explain.py b/explain.py index a9a1c4d..377da14 100644 --- a/explain.py +++ b/explain.py @@ -231,7 +231,14 @@ def explain_c2_beaconing(events: List[Dict], detected_ids: Set[str]) -> List[Dic ] avg_interval = _average(deltas) interval_cv = _cv(deltas) - ports = sorted({ev.get("dest_port") for ev in ordered if ev.get("dest_port")}) + ports = sorted( + { + dest_port + for ev in ordered + for dest_port in [ev.get("dest_port")] + if isinstance(dest_port, int) + } + ) max_bytes_out = max((ev.get("bytes_out", 0) for ev in ordered), default=0) max_bytes_in = max((ev.get("bytes_in", 0) for ev in ordered), default=0) @@ -279,7 +286,14 @@ def explain_lateral_movement(events: List[Dict], detected_ids: Set[str]) -> List continue ordered = sorted(smb_events, key=lambda e: e["timestamp"]) - unique_dests = sorted({e.get("dest_ip") for e in ordered if e.get("dest_ip")}) + unique_dests = sorted( + { + dest_ip + for e in ordered + for dest_ip in [e.get("dest_ip")] + if isinstance(dest_ip, str) + } + ) window_minutes = ( _minutes_between(ordered[0], ordered[-1]) if len(ordered) > 1 else 0.0 ) diff --git a/prepare.py b/prepare.py index 4671680..37353aa 100644 --- a/prepare.py +++ b/prepare.py @@ -1789,7 +1789,7 @@ def print_stats(events: list): print() # Attack type breakdown - attack_types = {} + attack_types: dict[str, int] = {} for e in malicious: at = e.get("attack_type", "unknown") attack_types[at] = attack_types.get(at, 0) + 1 @@ -1800,7 +1800,7 @@ def print_stats(events: list): # Event type breakdown print(f"\n Event types:") - event_types = {} + event_types: dict[str, int] = {} for e in events: et = e.get("event_type", "unknown") event_types[et] = event_types.get(et, 0) + 1 diff --git a/results/autoresearch-20260402-220009.json b/results/autoresearch-20260402-220009.json new file mode 100644 index 0000000..e53fd0c --- /dev/null +++ b/results/autoresearch-20260402-220009.json @@ -0,0 +1,256 @@ +{ + "generated_at": "2026-04-02T19:00:09.642469Z", + "seed": 1337, + "iters": 200, + "fp_max": 999999, + "penalty": 0.002, + "baseline": { + "f1_score": 0.864995, + "precision": 0.762108, + "recall": 1.0, + "false_positive_rate": 0.392105, + "accuracy": 0.826205, + "true_positives": 1432, + "false_positives": 447, + "true_negatives": 693, + "false_negatives": 0, + "total_events": 2572, + "total_detected": 1879 + }, + "best": { + "score": 0.874384614, + "f1": 0.875039, + "precision": 0.790096, + "recall": 0.980447, + "fp": 373, + "fn": 28, + "fpr": 0.327193, + "params": { + "brute_force": { + "RAPID_THRESHOLD": 12, + "RAPID_WINDOW_MINUTES": 15, + "SLOW_THRESHOLD": 2, + "SLOW_MIN_SPAN_MINUTES": 60 + }, + "dns_exfiltration": { + "MIN_QUERIES_PER_DOMAIN": 5, + "MIN_LABEL_LENGTH": 20, + "MIN_ENTROPY": 4.0, + "MIN_UNIQUE_LABEL_RATIO": 0.8 + }, + "c2_beaconing": { + "MIN_CONNECTIONS": 3, + "MAX_BYTES_OUT": 500, + "MAX_BYTES_IN": 400 + }, + "lateral_movement": { + "UNIQUE_DEST_THRESHOLD": 3, + "WINDOW_MINUTES": 30, + "MAX_AVERAGE_GAP_SECONDS": 180, + "MAX_TRANSFER_BYTES": 75000 + } + } + }, + "history_tail": [ + { + "iter": 181, + "f1": 0.831225, + "precision": 0.74461, + "recall": 0.940642, + "fp": 462, + "fn": 85, + "fpr": 0.405263, + "score": 0.830414474 + }, + { + "iter": 182, + "f1": 0.848337, + "precision": 0.753388, + "recall": 0.97067, + "fp": 455, + "fn": 42, + "fpr": 0.399123, + "score": 0.847538754 + }, + { + "iter": 183, + "f1": 0.719566, + "precision": 0.699407, + "recall": 0.740922, + "fp": 456, + "fn": 371, + "fpr": 0.4, + "score": 0.718766 + }, + { + "iter": 184, + "f1": 0.724208, + "precision": 0.70013, + "recall": 0.75, + "fp": 460, + "fn": 358, + "fpr": 0.403509, + "score": 0.7234009819999999 + }, + { + "iter": 185, + "f1": 0.726533, + "precision": 0.705727, + "recall": 0.748603, + "fp": 447, + "fn": 360, + "fpr": 0.392105, + "score": 0.72574879 + }, + { + "iter": 186, + "f1": 0.748641, + "precision": 0.728836, + "recall": 0.769553, + "fp": 410, + "fn": 330, + "fpr": 0.359649, + "score": 0.747921702 + }, + { + "iter": 187, + "f1": 0.838037, + "precision": 0.747265, + "recall": 0.953911, + "fp": 462, + "fn": 66, + "fpr": 0.405263, + "score": 0.837226474 + }, + { + "iter": 188, + "f1": 0.870958, + "precision": 0.779063, + "recall": 0.98743, + "fp": 401, + "fn": 18, + "fpr": 0.351754, + "score": 0.870254492 + }, + { + "iter": 189, + "f1": 0.848078, + "precision": 0.752979, + "recall": 0.97067, + "fp": 456, + "fn": 42, + "fpr": 0.4, + "score": 0.847278 + }, + { + "iter": 190, + "f1": 0.832767, + "precision": 0.747088, + "recall": 0.940642, + "fp": 456, + "fn": 85, + "fpr": 0.4, + "score": 0.831967 + }, + { + "iter": 191, + "f1": 0.735972, + "precision": 0.72573, + "recall": 0.746508, + "fp": 404, + "fn": 363, + "fpr": 0.354386, + "score": 0.735263228 + }, + { + "iter": 192, + "f1": 0.727642, + "precision": 0.706579, + "recall": 0.75, + "fp": 446, + "fn": 358, + "fpr": 0.391228, + "score": 0.726859544 + }, + { + "iter": 193, + "f1": 0.859903, + "precision": 0.757447, + "recall": 0.994413, + "fp": 456, + "fn": 8, + "fpr": 0.4, + "score": 0.859103 + }, + { + "iter": 194, + "f1": 0.685507, + "precision": 0.712349, + "recall": 0.660615, + "fp": 382, + "fn": 486, + "fpr": 0.335088, + "score": 0.684836824 + }, + { + "iter": 195, + "f1": 0.851899, + "precision": 0.778935, + "recall": 0.939944, + "fp": 382, + "fn": 86, + "fpr": 0.335088, + "score": 0.851228824 + }, + { + "iter": 196, + "f1": 0.709302, + "precision": 0.69504, + "recall": 0.724162, + "fp": 455, + "fn": 395, + "fpr": 0.399123, + "score": 0.708503754 + }, + { + "iter": 197, + "f1": 0.728859, + "precision": 0.70155, + "recall": 0.75838, + "fp": 462, + "fn": 346, + "fpr": 0.405263, + "score": 0.7280484740000001 + }, + { + "iter": 198, + "f1": 0.72678, + "precision": 0.706192, + "recall": 0.748603, + "fp": 446, + "fn": 360, + "fpr": 0.391228, + "score": 0.7259975439999999 + }, + { + "iter": 199, + "f1": 0.861725, + "precision": 0.760278, + "recall": 0.994413, + "fp": 449, + "fn": 8, + "fpr": 0.39386, + "score": 0.8609372799999999 + }, + { + "iter": 200, + "f1": 0.84178, + "precision": 0.754989, + "recall": 0.951117, + "fp": 442, + "fn": 70, + "fpr": 0.387719, + "score": 0.841004562 + } + ] +} \ No newline at end of file diff --git a/results/autoresearch-20260403-220011.json b/results/autoresearch-20260403-220011.json new file mode 100644 index 0000000..8c04c93 --- /dev/null +++ b/results/autoresearch-20260403-220011.json @@ -0,0 +1,256 @@ +{ + "generated_at": "2026-04-03T19:00:11.358616Z", + "seed": 1337, + "iters": 200, + "fp_max": 999999, + "penalty": 0.002, + "baseline": { + "f1_score": 0.864995, + "precision": 0.762108, + "recall": 1.0, + "false_positive_rate": 0.392105, + "accuracy": 0.826205, + "true_positives": 1432, + "false_positives": 447, + "true_negatives": 693, + "false_negatives": 0, + "total_events": 2572, + "total_detected": 1879 + }, + "best": { + "score": 0.874384614, + "f1": 0.875039, + "precision": 0.790096, + "recall": 0.980447, + "fp": 373, + "fn": 28, + "fpr": 0.327193, + "params": { + "brute_force": { + "RAPID_THRESHOLD": 12, + "RAPID_WINDOW_MINUTES": 15, + "SLOW_THRESHOLD": 2, + "SLOW_MIN_SPAN_MINUTES": 60 + }, + "dns_exfiltration": { + "MIN_QUERIES_PER_DOMAIN": 5, + "MIN_LABEL_LENGTH": 20, + "MIN_ENTROPY": 4.0, + "MIN_UNIQUE_LABEL_RATIO": 0.8 + }, + "c2_beaconing": { + "MIN_CONNECTIONS": 3, + "MAX_BYTES_OUT": 500, + "MAX_BYTES_IN": 400 + }, + "lateral_movement": { + "UNIQUE_DEST_THRESHOLD": 3, + "WINDOW_MINUTES": 30, + "MAX_AVERAGE_GAP_SECONDS": 180, + "MAX_TRANSFER_BYTES": 75000 + } + } + }, + "history_tail": [ + { + "iter": 181, + "f1": 0.831225, + "precision": 0.74461, + "recall": 0.940642, + "fp": 462, + "fn": 85, + "fpr": 0.405263, + "score": 0.830414474 + }, + { + "iter": 182, + "f1": 0.848337, + "precision": 0.753388, + "recall": 0.97067, + "fp": 455, + "fn": 42, + "fpr": 0.399123, + "score": 0.847538754 + }, + { + "iter": 183, + "f1": 0.719566, + "precision": 0.699407, + "recall": 0.740922, + "fp": 456, + "fn": 371, + "fpr": 0.4, + "score": 0.718766 + }, + { + "iter": 184, + "f1": 0.724208, + "precision": 0.70013, + "recall": 0.75, + "fp": 460, + "fn": 358, + "fpr": 0.403509, + "score": 0.7234009819999999 + }, + { + "iter": 185, + "f1": 0.726533, + "precision": 0.705727, + "recall": 0.748603, + "fp": 447, + "fn": 360, + "fpr": 0.392105, + "score": 0.72574879 + }, + { + "iter": 186, + "f1": 0.748641, + "precision": 0.728836, + "recall": 0.769553, + "fp": 410, + "fn": 330, + "fpr": 0.359649, + "score": 0.747921702 + }, + { + "iter": 187, + "f1": 0.838037, + "precision": 0.747265, + "recall": 0.953911, + "fp": 462, + "fn": 66, + "fpr": 0.405263, + "score": 0.837226474 + }, + { + "iter": 188, + "f1": 0.870958, + "precision": 0.779063, + "recall": 0.98743, + "fp": 401, + "fn": 18, + "fpr": 0.351754, + "score": 0.870254492 + }, + { + "iter": 189, + "f1": 0.848078, + "precision": 0.752979, + "recall": 0.97067, + "fp": 456, + "fn": 42, + "fpr": 0.4, + "score": 0.847278 + }, + { + "iter": 190, + "f1": 0.832767, + "precision": 0.747088, + "recall": 0.940642, + "fp": 456, + "fn": 85, + "fpr": 0.4, + "score": 0.831967 + }, + { + "iter": 191, + "f1": 0.735972, + "precision": 0.72573, + "recall": 0.746508, + "fp": 404, + "fn": 363, + "fpr": 0.354386, + "score": 0.735263228 + }, + { + "iter": 192, + "f1": 0.727642, + "precision": 0.706579, + "recall": 0.75, + "fp": 446, + "fn": 358, + "fpr": 0.391228, + "score": 0.726859544 + }, + { + "iter": 193, + "f1": 0.859903, + "precision": 0.757447, + "recall": 0.994413, + "fp": 456, + "fn": 8, + "fpr": 0.4, + "score": 0.859103 + }, + { + "iter": 194, + "f1": 0.685507, + "precision": 0.712349, + "recall": 0.660615, + "fp": 382, + "fn": 486, + "fpr": 0.335088, + "score": 0.684836824 + }, + { + "iter": 195, + "f1": 0.851899, + "precision": 0.778935, + "recall": 0.939944, + "fp": 382, + "fn": 86, + "fpr": 0.335088, + "score": 0.851228824 + }, + { + "iter": 196, + "f1": 0.709302, + "precision": 0.69504, + "recall": 0.724162, + "fp": 455, + "fn": 395, + "fpr": 0.399123, + "score": 0.708503754 + }, + { + "iter": 197, + "f1": 0.728859, + "precision": 0.70155, + "recall": 0.75838, + "fp": 462, + "fn": 346, + "fpr": 0.405263, + "score": 0.7280484740000001 + }, + { + "iter": 198, + "f1": 0.72678, + "precision": 0.706192, + "recall": 0.748603, + "fp": 446, + "fn": 360, + "fpr": 0.391228, + "score": 0.7259975439999999 + }, + { + "iter": 199, + "f1": 0.861725, + "precision": 0.760278, + "recall": 0.994413, + "fp": 449, + "fn": 8, + "fpr": 0.39386, + "score": 0.8609372799999999 + }, + { + "iter": 200, + "f1": 0.84178, + "precision": 0.754989, + "recall": 0.951117, + "fp": 442, + "fn": 70, + "fpr": 0.387719, + "score": 0.841004562 + } + ] +} \ No newline at end of file diff --git a/results/autoresearch-20260404-220009.json b/results/autoresearch-20260404-220009.json new file mode 100644 index 0000000..671cadf --- /dev/null +++ b/results/autoresearch-20260404-220009.json @@ -0,0 +1,256 @@ +{ + "generated_at": "2026-04-04T19:00:09.415791Z", + "seed": 1337, + "iters": 200, + "fp_max": 999999, + "penalty": 0.002, + "baseline": { + "f1_score": 0.864995, + "precision": 0.762108, + "recall": 1.0, + "false_positive_rate": 0.392105, + "accuracy": 0.826205, + "true_positives": 1432, + "false_positives": 447, + "true_negatives": 693, + "false_negatives": 0, + "total_events": 2572, + "total_detected": 1879 + }, + "best": { + "score": 0.874384614, + "f1": 0.875039, + "precision": 0.790096, + "recall": 0.980447, + "fp": 373, + "fn": 28, + "fpr": 0.327193, + "params": { + "brute_force": { + "RAPID_THRESHOLD": 12, + "RAPID_WINDOW_MINUTES": 15, + "SLOW_THRESHOLD": 2, + "SLOW_MIN_SPAN_MINUTES": 60 + }, + "dns_exfiltration": { + "MIN_QUERIES_PER_DOMAIN": 5, + "MIN_LABEL_LENGTH": 20, + "MIN_ENTROPY": 4.0, + "MIN_UNIQUE_LABEL_RATIO": 0.8 + }, + "c2_beaconing": { + "MIN_CONNECTIONS": 3, + "MAX_BYTES_OUT": 500, + "MAX_BYTES_IN": 400 + }, + "lateral_movement": { + "UNIQUE_DEST_THRESHOLD": 3, + "WINDOW_MINUTES": 30, + "MAX_AVERAGE_GAP_SECONDS": 180, + "MAX_TRANSFER_BYTES": 75000 + } + } + }, + "history_tail": [ + { + "iter": 181, + "f1": 0.831225, + "precision": 0.74461, + "recall": 0.940642, + "fp": 462, + "fn": 85, + "fpr": 0.405263, + "score": 0.830414474 + }, + { + "iter": 182, + "f1": 0.848337, + "precision": 0.753388, + "recall": 0.97067, + "fp": 455, + "fn": 42, + "fpr": 0.399123, + "score": 0.847538754 + }, + { + "iter": 183, + "f1": 0.719566, + "precision": 0.699407, + "recall": 0.740922, + "fp": 456, + "fn": 371, + "fpr": 0.4, + "score": 0.718766 + }, + { + "iter": 184, + "f1": 0.724208, + "precision": 0.70013, + "recall": 0.75, + "fp": 460, + "fn": 358, + "fpr": 0.403509, + "score": 0.7234009819999999 + }, + { + "iter": 185, + "f1": 0.726533, + "precision": 0.705727, + "recall": 0.748603, + "fp": 447, + "fn": 360, + "fpr": 0.392105, + "score": 0.72574879 + }, + { + "iter": 186, + "f1": 0.748641, + "precision": 0.728836, + "recall": 0.769553, + "fp": 410, + "fn": 330, + "fpr": 0.359649, + "score": 0.747921702 + }, + { + "iter": 187, + "f1": 0.838037, + "precision": 0.747265, + "recall": 0.953911, + "fp": 462, + "fn": 66, + "fpr": 0.405263, + "score": 0.837226474 + }, + { + "iter": 188, + "f1": 0.870958, + "precision": 0.779063, + "recall": 0.98743, + "fp": 401, + "fn": 18, + "fpr": 0.351754, + "score": 0.870254492 + }, + { + "iter": 189, + "f1": 0.848078, + "precision": 0.752979, + "recall": 0.97067, + "fp": 456, + "fn": 42, + "fpr": 0.4, + "score": 0.847278 + }, + { + "iter": 190, + "f1": 0.832767, + "precision": 0.747088, + "recall": 0.940642, + "fp": 456, + "fn": 85, + "fpr": 0.4, + "score": 0.831967 + }, + { + "iter": 191, + "f1": 0.735972, + "precision": 0.72573, + "recall": 0.746508, + "fp": 404, + "fn": 363, + "fpr": 0.354386, + "score": 0.735263228 + }, + { + "iter": 192, + "f1": 0.727642, + "precision": 0.706579, + "recall": 0.75, + "fp": 446, + "fn": 358, + "fpr": 0.391228, + "score": 0.726859544 + }, + { + "iter": 193, + "f1": 0.859903, + "precision": 0.757447, + "recall": 0.994413, + "fp": 456, + "fn": 8, + "fpr": 0.4, + "score": 0.859103 + }, + { + "iter": 194, + "f1": 0.685507, + "precision": 0.712349, + "recall": 0.660615, + "fp": 382, + "fn": 486, + "fpr": 0.335088, + "score": 0.684836824 + }, + { + "iter": 195, + "f1": 0.851899, + "precision": 0.778935, + "recall": 0.939944, + "fp": 382, + "fn": 86, + "fpr": 0.335088, + "score": 0.851228824 + }, + { + "iter": 196, + "f1": 0.709302, + "precision": 0.69504, + "recall": 0.724162, + "fp": 455, + "fn": 395, + "fpr": 0.399123, + "score": 0.708503754 + }, + { + "iter": 197, + "f1": 0.728859, + "precision": 0.70155, + "recall": 0.75838, + "fp": 462, + "fn": 346, + "fpr": 0.405263, + "score": 0.7280484740000001 + }, + { + "iter": 198, + "f1": 0.72678, + "precision": 0.706192, + "recall": 0.748603, + "fp": 446, + "fn": 360, + "fpr": 0.391228, + "score": 0.7259975439999999 + }, + { + "iter": 199, + "f1": 0.861725, + "precision": 0.760278, + "recall": 0.994413, + "fp": 449, + "fn": 8, + "fpr": 0.39386, + "score": 0.8609372799999999 + }, + { + "iter": 200, + "f1": 0.84178, + "precision": 0.754989, + "recall": 0.951117, + "fp": 442, + "fn": 70, + "fpr": 0.387719, + "score": 0.841004562 + } + ] +} \ No newline at end of file diff --git a/results/autoresearch-20260405-220010.json b/results/autoresearch-20260405-220010.json new file mode 100644 index 0000000..39fdd33 --- /dev/null +++ b/results/autoresearch-20260405-220010.json @@ -0,0 +1,256 @@ +{ + "generated_at": "2026-04-05T19:00:10.846348Z", + "seed": 1337, + "iters": 200, + "fp_max": 999999, + "penalty": 0.002, + "baseline": { + "f1_score": 0.864995, + "precision": 0.762108, + "recall": 1.0, + "false_positive_rate": 0.392105, + "accuracy": 0.826205, + "true_positives": 1432, + "false_positives": 447, + "true_negatives": 693, + "false_negatives": 0, + "total_events": 2572, + "total_detected": 1879 + }, + "best": { + "score": 0.874384614, + "f1": 0.875039, + "precision": 0.790096, + "recall": 0.980447, + "fp": 373, + "fn": 28, + "fpr": 0.327193, + "params": { + "brute_force": { + "RAPID_THRESHOLD": 12, + "RAPID_WINDOW_MINUTES": 15, + "SLOW_THRESHOLD": 2, + "SLOW_MIN_SPAN_MINUTES": 60 + }, + "dns_exfiltration": { + "MIN_QUERIES_PER_DOMAIN": 5, + "MIN_LABEL_LENGTH": 20, + "MIN_ENTROPY": 4.0, + "MIN_UNIQUE_LABEL_RATIO": 0.8 + }, + "c2_beaconing": { + "MIN_CONNECTIONS": 3, + "MAX_BYTES_OUT": 500, + "MAX_BYTES_IN": 400 + }, + "lateral_movement": { + "UNIQUE_DEST_THRESHOLD": 3, + "WINDOW_MINUTES": 30, + "MAX_AVERAGE_GAP_SECONDS": 180, + "MAX_TRANSFER_BYTES": 75000 + } + } + }, + "history_tail": [ + { + "iter": 181, + "f1": 0.831225, + "precision": 0.74461, + "recall": 0.940642, + "fp": 462, + "fn": 85, + "fpr": 0.405263, + "score": 0.830414474 + }, + { + "iter": 182, + "f1": 0.848337, + "precision": 0.753388, + "recall": 0.97067, + "fp": 455, + "fn": 42, + "fpr": 0.399123, + "score": 0.847538754 + }, + { + "iter": 183, + "f1": 0.719566, + "precision": 0.699407, + "recall": 0.740922, + "fp": 456, + "fn": 371, + "fpr": 0.4, + "score": 0.718766 + }, + { + "iter": 184, + "f1": 0.724208, + "precision": 0.70013, + "recall": 0.75, + "fp": 460, + "fn": 358, + "fpr": 0.403509, + "score": 0.7234009819999999 + }, + { + "iter": 185, + "f1": 0.726533, + "precision": 0.705727, + "recall": 0.748603, + "fp": 447, + "fn": 360, + "fpr": 0.392105, + "score": 0.72574879 + }, + { + "iter": 186, + "f1": 0.748641, + "precision": 0.728836, + "recall": 0.769553, + "fp": 410, + "fn": 330, + "fpr": 0.359649, + "score": 0.747921702 + }, + { + "iter": 187, + "f1": 0.838037, + "precision": 0.747265, + "recall": 0.953911, + "fp": 462, + "fn": 66, + "fpr": 0.405263, + "score": 0.837226474 + }, + { + "iter": 188, + "f1": 0.870958, + "precision": 0.779063, + "recall": 0.98743, + "fp": 401, + "fn": 18, + "fpr": 0.351754, + "score": 0.870254492 + }, + { + "iter": 189, + "f1": 0.848078, + "precision": 0.752979, + "recall": 0.97067, + "fp": 456, + "fn": 42, + "fpr": 0.4, + "score": 0.847278 + }, + { + "iter": 190, + "f1": 0.832767, + "precision": 0.747088, + "recall": 0.940642, + "fp": 456, + "fn": 85, + "fpr": 0.4, + "score": 0.831967 + }, + { + "iter": 191, + "f1": 0.735972, + "precision": 0.72573, + "recall": 0.746508, + "fp": 404, + "fn": 363, + "fpr": 0.354386, + "score": 0.735263228 + }, + { + "iter": 192, + "f1": 0.727642, + "precision": 0.706579, + "recall": 0.75, + "fp": 446, + "fn": 358, + "fpr": 0.391228, + "score": 0.726859544 + }, + { + "iter": 193, + "f1": 0.859903, + "precision": 0.757447, + "recall": 0.994413, + "fp": 456, + "fn": 8, + "fpr": 0.4, + "score": 0.859103 + }, + { + "iter": 194, + "f1": 0.685507, + "precision": 0.712349, + "recall": 0.660615, + "fp": 382, + "fn": 486, + "fpr": 0.335088, + "score": 0.684836824 + }, + { + "iter": 195, + "f1": 0.851899, + "precision": 0.778935, + "recall": 0.939944, + "fp": 382, + "fn": 86, + "fpr": 0.335088, + "score": 0.851228824 + }, + { + "iter": 196, + "f1": 0.709302, + "precision": 0.69504, + "recall": 0.724162, + "fp": 455, + "fn": 395, + "fpr": 0.399123, + "score": 0.708503754 + }, + { + "iter": 197, + "f1": 0.728859, + "precision": 0.70155, + "recall": 0.75838, + "fp": 462, + "fn": 346, + "fpr": 0.405263, + "score": 0.7280484740000001 + }, + { + "iter": 198, + "f1": 0.72678, + "precision": 0.706192, + "recall": 0.748603, + "fp": 446, + "fn": 360, + "fpr": 0.391228, + "score": 0.7259975439999999 + }, + { + "iter": 199, + "f1": 0.861725, + "precision": 0.760278, + "recall": 0.994413, + "fp": 449, + "fn": 8, + "fpr": 0.39386, + "score": 0.8609372799999999 + }, + { + "iter": 200, + "f1": 0.84178, + "precision": 0.754989, + "recall": 0.951117, + "fp": 442, + "fn": 70, + "fpr": 0.387719, + "score": 0.841004562 + } + ] +} \ No newline at end of file diff --git a/results/autoresearch-20260406-220009.json b/results/autoresearch-20260406-220009.json new file mode 100644 index 0000000..0fcd958 --- /dev/null +++ b/results/autoresearch-20260406-220009.json @@ -0,0 +1,256 @@ +{ + "generated_at": "2026-04-06T19:00:09.581500Z", + "seed": 1337, + "iters": 200, + "fp_max": 999999, + "penalty": 0.002, + "baseline": { + "f1_score": 0.864995, + "precision": 0.762108, + "recall": 1.0, + "false_positive_rate": 0.392105, + "accuracy": 0.826205, + "true_positives": 1432, + "false_positives": 447, + "true_negatives": 693, + "false_negatives": 0, + "total_events": 2572, + "total_detected": 1879 + }, + "best": { + "score": 0.874384614, + "f1": 0.875039, + "precision": 0.790096, + "recall": 0.980447, + "fp": 373, + "fn": 28, + "fpr": 0.327193, + "params": { + "brute_force": { + "RAPID_THRESHOLD": 12, + "RAPID_WINDOW_MINUTES": 15, + "SLOW_THRESHOLD": 2, + "SLOW_MIN_SPAN_MINUTES": 60 + }, + "dns_exfiltration": { + "MIN_QUERIES_PER_DOMAIN": 5, + "MIN_LABEL_LENGTH": 20, + "MIN_ENTROPY": 4.0, + "MIN_UNIQUE_LABEL_RATIO": 0.8 + }, + "c2_beaconing": { + "MIN_CONNECTIONS": 3, + "MAX_BYTES_OUT": 500, + "MAX_BYTES_IN": 400 + }, + "lateral_movement": { + "UNIQUE_DEST_THRESHOLD": 3, + "WINDOW_MINUTES": 30, + "MAX_AVERAGE_GAP_SECONDS": 180, + "MAX_TRANSFER_BYTES": 75000 + } + } + }, + "history_tail": [ + { + "iter": 181, + "f1": 0.831225, + "precision": 0.74461, + "recall": 0.940642, + "fp": 462, + "fn": 85, + "fpr": 0.405263, + "score": 0.830414474 + }, + { + "iter": 182, + "f1": 0.848337, + "precision": 0.753388, + "recall": 0.97067, + "fp": 455, + "fn": 42, + "fpr": 0.399123, + "score": 0.847538754 + }, + { + "iter": 183, + "f1": 0.719566, + "precision": 0.699407, + "recall": 0.740922, + "fp": 456, + "fn": 371, + "fpr": 0.4, + "score": 0.718766 + }, + { + "iter": 184, + "f1": 0.724208, + "precision": 0.70013, + "recall": 0.75, + "fp": 460, + "fn": 358, + "fpr": 0.403509, + "score": 0.7234009819999999 + }, + { + "iter": 185, + "f1": 0.726533, + "precision": 0.705727, + "recall": 0.748603, + "fp": 447, + "fn": 360, + "fpr": 0.392105, + "score": 0.72574879 + }, + { + "iter": 186, + "f1": 0.748641, + "precision": 0.728836, + "recall": 0.769553, + "fp": 410, + "fn": 330, + "fpr": 0.359649, + "score": 0.747921702 + }, + { + "iter": 187, + "f1": 0.838037, + "precision": 0.747265, + "recall": 0.953911, + "fp": 462, + "fn": 66, + "fpr": 0.405263, + "score": 0.837226474 + }, + { + "iter": 188, + "f1": 0.870958, + "precision": 0.779063, + "recall": 0.98743, + "fp": 401, + "fn": 18, + "fpr": 0.351754, + "score": 0.870254492 + }, + { + "iter": 189, + "f1": 0.848078, + "precision": 0.752979, + "recall": 0.97067, + "fp": 456, + "fn": 42, + "fpr": 0.4, + "score": 0.847278 + }, + { + "iter": 190, + "f1": 0.832767, + "precision": 0.747088, + "recall": 0.940642, + "fp": 456, + "fn": 85, + "fpr": 0.4, + "score": 0.831967 + }, + { + "iter": 191, + "f1": 0.735972, + "precision": 0.72573, + "recall": 0.746508, + "fp": 404, + "fn": 363, + "fpr": 0.354386, + "score": 0.735263228 + }, + { + "iter": 192, + "f1": 0.727642, + "precision": 0.706579, + "recall": 0.75, + "fp": 446, + "fn": 358, + "fpr": 0.391228, + "score": 0.726859544 + }, + { + "iter": 193, + "f1": 0.859903, + "precision": 0.757447, + "recall": 0.994413, + "fp": 456, + "fn": 8, + "fpr": 0.4, + "score": 0.859103 + }, + { + "iter": 194, + "f1": 0.685507, + "precision": 0.712349, + "recall": 0.660615, + "fp": 382, + "fn": 486, + "fpr": 0.335088, + "score": 0.684836824 + }, + { + "iter": 195, + "f1": 0.851899, + "precision": 0.778935, + "recall": 0.939944, + "fp": 382, + "fn": 86, + "fpr": 0.335088, + "score": 0.851228824 + }, + { + "iter": 196, + "f1": 0.709302, + "precision": 0.69504, + "recall": 0.724162, + "fp": 455, + "fn": 395, + "fpr": 0.399123, + "score": 0.708503754 + }, + { + "iter": 197, + "f1": 0.728859, + "precision": 0.70155, + "recall": 0.75838, + "fp": 462, + "fn": 346, + "fpr": 0.405263, + "score": 0.7280484740000001 + }, + { + "iter": 198, + "f1": 0.72678, + "precision": 0.706192, + "recall": 0.748603, + "fp": 446, + "fn": 360, + "fpr": 0.391228, + "score": 0.7259975439999999 + }, + { + "iter": 199, + "f1": 0.861725, + "precision": 0.760278, + "recall": 0.994413, + "fp": 449, + "fn": 8, + "fpr": 0.39386, + "score": 0.8609372799999999 + }, + { + "iter": 200, + "f1": 0.84178, + "precision": 0.754989, + "recall": 0.951117, + "fp": 442, + "fn": 70, + "fpr": 0.387719, + "score": 0.841004562 + } + ] +} \ No newline at end of file diff --git a/results/autoresearch-20260407-220010.json b/results/autoresearch-20260407-220010.json new file mode 100644 index 0000000..04c3219 --- /dev/null +++ b/results/autoresearch-20260407-220010.json @@ -0,0 +1,256 @@ +{ + "generated_at": "2026-04-07T19:00:10.259624Z", + "seed": 1337, + "iters": 200, + "fp_max": 999999, + "penalty": 0.002, + "baseline": { + "f1_score": 0.864995, + "precision": 0.762108, + "recall": 1.0, + "false_positive_rate": 0.392105, + "accuracy": 0.826205, + "true_positives": 1432, + "false_positives": 447, + "true_negatives": 693, + "false_negatives": 0, + "total_events": 2572, + "total_detected": 1879 + }, + "best": { + "score": 0.874384614, + "f1": 0.875039, + "precision": 0.790096, + "recall": 0.980447, + "fp": 373, + "fn": 28, + "fpr": 0.327193, + "params": { + "brute_force": { + "RAPID_THRESHOLD": 12, + "RAPID_WINDOW_MINUTES": 15, + "SLOW_THRESHOLD": 2, + "SLOW_MIN_SPAN_MINUTES": 60 + }, + "dns_exfiltration": { + "MIN_QUERIES_PER_DOMAIN": 5, + "MIN_LABEL_LENGTH": 20, + "MIN_ENTROPY": 4.0, + "MIN_UNIQUE_LABEL_RATIO": 0.8 + }, + "c2_beaconing": { + "MIN_CONNECTIONS": 3, + "MAX_BYTES_OUT": 500, + "MAX_BYTES_IN": 400 + }, + "lateral_movement": { + "UNIQUE_DEST_THRESHOLD": 3, + "WINDOW_MINUTES": 30, + "MAX_AVERAGE_GAP_SECONDS": 180, + "MAX_TRANSFER_BYTES": 75000 + } + } + }, + "history_tail": [ + { + "iter": 181, + "f1": 0.831225, + "precision": 0.74461, + "recall": 0.940642, + "fp": 462, + "fn": 85, + "fpr": 0.405263, + "score": 0.830414474 + }, + { + "iter": 182, + "f1": 0.848337, + "precision": 0.753388, + "recall": 0.97067, + "fp": 455, + "fn": 42, + "fpr": 0.399123, + "score": 0.847538754 + }, + { + "iter": 183, + "f1": 0.719566, + "precision": 0.699407, + "recall": 0.740922, + "fp": 456, + "fn": 371, + "fpr": 0.4, + "score": 0.718766 + }, + { + "iter": 184, + "f1": 0.724208, + "precision": 0.70013, + "recall": 0.75, + "fp": 460, + "fn": 358, + "fpr": 0.403509, + "score": 0.7234009819999999 + }, + { + "iter": 185, + "f1": 0.726533, + "precision": 0.705727, + "recall": 0.748603, + "fp": 447, + "fn": 360, + "fpr": 0.392105, + "score": 0.72574879 + }, + { + "iter": 186, + "f1": 0.748641, + "precision": 0.728836, + "recall": 0.769553, + "fp": 410, + "fn": 330, + "fpr": 0.359649, + "score": 0.747921702 + }, + { + "iter": 187, + "f1": 0.838037, + "precision": 0.747265, + "recall": 0.953911, + "fp": 462, + "fn": 66, + "fpr": 0.405263, + "score": 0.837226474 + }, + { + "iter": 188, + "f1": 0.870958, + "precision": 0.779063, + "recall": 0.98743, + "fp": 401, + "fn": 18, + "fpr": 0.351754, + "score": 0.870254492 + }, + { + "iter": 189, + "f1": 0.848078, + "precision": 0.752979, + "recall": 0.97067, + "fp": 456, + "fn": 42, + "fpr": 0.4, + "score": 0.847278 + }, + { + "iter": 190, + "f1": 0.832767, + "precision": 0.747088, + "recall": 0.940642, + "fp": 456, + "fn": 85, + "fpr": 0.4, + "score": 0.831967 + }, + { + "iter": 191, + "f1": 0.735972, + "precision": 0.72573, + "recall": 0.746508, + "fp": 404, + "fn": 363, + "fpr": 0.354386, + "score": 0.735263228 + }, + { + "iter": 192, + "f1": 0.727642, + "precision": 0.706579, + "recall": 0.75, + "fp": 446, + "fn": 358, + "fpr": 0.391228, + "score": 0.726859544 + }, + { + "iter": 193, + "f1": 0.859903, + "precision": 0.757447, + "recall": 0.994413, + "fp": 456, + "fn": 8, + "fpr": 0.4, + "score": 0.859103 + }, + { + "iter": 194, + "f1": 0.685507, + "precision": 0.712349, + "recall": 0.660615, + "fp": 382, + "fn": 486, + "fpr": 0.335088, + "score": 0.684836824 + }, + { + "iter": 195, + "f1": 0.851899, + "precision": 0.778935, + "recall": 0.939944, + "fp": 382, + "fn": 86, + "fpr": 0.335088, + "score": 0.851228824 + }, + { + "iter": 196, + "f1": 0.709302, + "precision": 0.69504, + "recall": 0.724162, + "fp": 455, + "fn": 395, + "fpr": 0.399123, + "score": 0.708503754 + }, + { + "iter": 197, + "f1": 0.728859, + "precision": 0.70155, + "recall": 0.75838, + "fp": 462, + "fn": 346, + "fpr": 0.405263, + "score": 0.7280484740000001 + }, + { + "iter": 198, + "f1": 0.72678, + "precision": 0.706192, + "recall": 0.748603, + "fp": 446, + "fn": 360, + "fpr": 0.391228, + "score": 0.7259975439999999 + }, + { + "iter": 199, + "f1": 0.861725, + "precision": 0.760278, + "recall": 0.994413, + "fp": 449, + "fn": 8, + "fpr": 0.39386, + "score": 0.8609372799999999 + }, + { + "iter": 200, + "f1": 0.84178, + "precision": 0.754989, + "recall": 0.951117, + "fp": 442, + "fn": 70, + "fpr": 0.387719, + "score": 0.841004562 + } + ] +} \ No newline at end of file diff --git a/results/autoresearch-20260411-220009.json b/results/autoresearch-20260411-220009.json new file mode 100644 index 0000000..cf03822 --- /dev/null +++ b/results/autoresearch-20260411-220009.json @@ -0,0 +1,256 @@ +{ + "generated_at": "2026-04-11T19:00:09.708483Z", + "seed": 1337, + "iters": 200, + "fp_max": 999999, + "penalty": 0.002, + "baseline": { + "f1_score": 0.864995, + "precision": 0.762108, + "recall": 1.0, + "false_positive_rate": 0.392105, + "accuracy": 0.826205, + "true_positives": 1432, + "false_positives": 447, + "true_negatives": 693, + "false_negatives": 0, + "total_events": 2572, + "total_detected": 1879 + }, + "best": { + "score": 0.874384614, + "f1": 0.875039, + "precision": 0.790096, + "recall": 0.980447, + "fp": 373, + "fn": 28, + "fpr": 0.327193, + "params": { + "brute_force": { + "RAPID_THRESHOLD": 12, + "RAPID_WINDOW_MINUTES": 15, + "SLOW_THRESHOLD": 2, + "SLOW_MIN_SPAN_MINUTES": 60 + }, + "dns_exfiltration": { + "MIN_QUERIES_PER_DOMAIN": 5, + "MIN_LABEL_LENGTH": 20, + "MIN_ENTROPY": 4.0, + "MIN_UNIQUE_LABEL_RATIO": 0.8 + }, + "c2_beaconing": { + "MIN_CONNECTIONS": 3, + "MAX_BYTES_OUT": 500, + "MAX_BYTES_IN": 400 + }, + "lateral_movement": { + "UNIQUE_DEST_THRESHOLD": 3, + "WINDOW_MINUTES": 30, + "MAX_AVERAGE_GAP_SECONDS": 180, + "MAX_TRANSFER_BYTES": 75000 + } + } + }, + "history_tail": [ + { + "iter": 181, + "f1": 0.831225, + "precision": 0.74461, + "recall": 0.940642, + "fp": 462, + "fn": 85, + "fpr": 0.405263, + "score": 0.830414474 + }, + { + "iter": 182, + "f1": 0.848337, + "precision": 0.753388, + "recall": 0.97067, + "fp": 455, + "fn": 42, + "fpr": 0.399123, + "score": 0.847538754 + }, + { + "iter": 183, + "f1": 0.719566, + "precision": 0.699407, + "recall": 0.740922, + "fp": 456, + "fn": 371, + "fpr": 0.4, + "score": 0.718766 + }, + { + "iter": 184, + "f1": 0.724208, + "precision": 0.70013, + "recall": 0.75, + "fp": 460, + "fn": 358, + "fpr": 0.403509, + "score": 0.7234009819999999 + }, + { + "iter": 185, + "f1": 0.726533, + "precision": 0.705727, + "recall": 0.748603, + "fp": 447, + "fn": 360, + "fpr": 0.392105, + "score": 0.72574879 + }, + { + "iter": 186, + "f1": 0.748641, + "precision": 0.728836, + "recall": 0.769553, + "fp": 410, + "fn": 330, + "fpr": 0.359649, + "score": 0.747921702 + }, + { + "iter": 187, + "f1": 0.838037, + "precision": 0.747265, + "recall": 0.953911, + "fp": 462, + "fn": 66, + "fpr": 0.405263, + "score": 0.837226474 + }, + { + "iter": 188, + "f1": 0.870958, + "precision": 0.779063, + "recall": 0.98743, + "fp": 401, + "fn": 18, + "fpr": 0.351754, + "score": 0.870254492 + }, + { + "iter": 189, + "f1": 0.848078, + "precision": 0.752979, + "recall": 0.97067, + "fp": 456, + "fn": 42, + "fpr": 0.4, + "score": 0.847278 + }, + { + "iter": 190, + "f1": 0.832767, + "precision": 0.747088, + "recall": 0.940642, + "fp": 456, + "fn": 85, + "fpr": 0.4, + "score": 0.831967 + }, + { + "iter": 191, + "f1": 0.735972, + "precision": 0.72573, + "recall": 0.746508, + "fp": 404, + "fn": 363, + "fpr": 0.354386, + "score": 0.735263228 + }, + { + "iter": 192, + "f1": 0.727642, + "precision": 0.706579, + "recall": 0.75, + "fp": 446, + "fn": 358, + "fpr": 0.391228, + "score": 0.726859544 + }, + { + "iter": 193, + "f1": 0.859903, + "precision": 0.757447, + "recall": 0.994413, + "fp": 456, + "fn": 8, + "fpr": 0.4, + "score": 0.859103 + }, + { + "iter": 194, + "f1": 0.685507, + "precision": 0.712349, + "recall": 0.660615, + "fp": 382, + "fn": 486, + "fpr": 0.335088, + "score": 0.684836824 + }, + { + "iter": 195, + "f1": 0.851899, + "precision": 0.778935, + "recall": 0.939944, + "fp": 382, + "fn": 86, + "fpr": 0.335088, + "score": 0.851228824 + }, + { + "iter": 196, + "f1": 0.709302, + "precision": 0.69504, + "recall": 0.724162, + "fp": 455, + "fn": 395, + "fpr": 0.399123, + "score": 0.708503754 + }, + { + "iter": 197, + "f1": 0.728859, + "precision": 0.70155, + "recall": 0.75838, + "fp": 462, + "fn": 346, + "fpr": 0.405263, + "score": 0.7280484740000001 + }, + { + "iter": 198, + "f1": 0.72678, + "precision": 0.706192, + "recall": 0.748603, + "fp": 446, + "fn": 360, + "fpr": 0.391228, + "score": 0.7259975439999999 + }, + { + "iter": 199, + "f1": 0.861725, + "precision": 0.760278, + "recall": 0.994413, + "fp": 449, + "fn": 8, + "fpr": 0.39386, + "score": 0.8609372799999999 + }, + { + "iter": 200, + "f1": 0.84178, + "precision": 0.754989, + "recall": 0.951117, + "fp": 442, + "fn": 70, + "fpr": 0.387719, + "score": 0.841004562 + } + ] +} \ No newline at end of file diff --git a/scripts/sync_findings_to_supabase.py b/scripts/sync_findings_to_supabase.py index da2d1a3..11d144e 100644 --- a/scripts/sync_findings_to_supabase.py +++ b/scripts/sync_findings_to_supabase.py @@ -44,20 +44,22 @@ "disposition", "confidence", "source", - "source_name", - "detector", - "fingerprint", - "dedupe_key", + "source_platform", + "correlation_type", + "detection_layer", "detected_at", "first_seen_at", "last_seen_at", "rule_id", "rule_name", "mitre", + "mitre_ids", "event_count", "event_ids", + "risk_tags", "recommended_actions", "raw_payload", + "metadata", } @@ -234,16 +236,42 @@ def compact_fingerprint(finding: dict[str, Any]) -> str | None: return None -def normalize_confidence(finding: dict[str, Any]) -> float | None: - for key in ("confidence", "confidence_score", "score"): +def normalize_confidence(finding: dict[str, Any]) -> str: + for key in ("confidence", "confidence_score", "score", "severity_score"): value = finding.get(key) if value is None: continue try: - return float(value) + score = float(value) + if score >= 80: + return "high" + if score >= 50: + return "medium" + return "low" except (TypeError, ValueError): - continue - return None + text = str(value).strip().lower() + if text in {"low", "medium", "high"}: + return text + return "medium" + + +def normalize_source_fields(finding: dict[str, Any]) -> tuple[str, str, str | None, str]: + raw_source = str(finding.get("source") or "").strip().lower() + platform = str(finding.get("platform") or raw_source or "openclaw").strip().lower() + platform = platform if platform in {"openclaw", "macos", "linux", "windows"} else "openclaw" + + source = raw_source if raw_source in {"openclaw", "macos", "linux", "windows", "correlated"} else platform + if source not in {"openclaw", "macos", "linux", "windows", "correlated"}: + source = "openclaw" + + detection_layer = "correlated" if source == "correlated" else ( + "application" if source == "openclaw" else "host" + ) + correlation_type = finding.get("correlation_type") + if source != "correlated": + correlation_type = None + + return source, platform, correlation_type, detection_layer def normalize_row(finding: dict[str, Any]) -> dict[str, Any] | None: @@ -251,35 +279,50 @@ def normalize_row(finding: dict[str, Any]) -> dict[str, Any] | None: if not external_finding_id: return None - source = finding.get("source") - source_name = None - if isinstance(source, str) and source: - source_name = Path(source).name + source, source_platform, correlation_type, detection_layer = normalize_source_fields(finding) + metadata = { + "source_name": Path(str(finding.get("source"))).name if isinstance(finding.get("source"), str) and finding.get("source") else None, + "detector": finding.get("rule_name") or finding.get("detector") or None, + "fingerprint": compact_fingerprint(finding), + "dedupe_key": finding.get("dedupe_key") or finding.get("finding_id") or None, + } + metadata = {k: v for k, v in metadata.items() if v not in (None, "", [], {})} + + mitre = finding.get("mitre") + mitre_ids = finding.get("mitre_ids") + if not mitre_ids and mitre: + mitre_ids = [mitre] + + risk_tags = finding.get("risk_tags") or [] + if not risk_tags: + risk_tags = list(metadata.keys()) if metadata else [] row = { "external_finding_id": str(external_finding_id), "title": str(finding.get("title") or finding.get("rule_name") or finding.get("name") or "Untitled finding"), - "summary": finding.get("summary") or finding.get("description") or None, + "summary": finding.get("summary") or finding.get("description") or "No summary provided.", "severity": str(finding.get("severity") or "low"), "severity_score": finding.get("severity_score"), "status": str(finding.get("status") or finding.get("triage_status") or "open"), - "disposition": finding.get("disposition"), + "disposition": str(finding.get("disposition") or "unreviewed"), "confidence": normalize_confidence(finding), "source": source, - "source_name": source_name, - "detector": finding.get("rule_name") or finding.get("detector") or None, - "fingerprint": compact_fingerprint(finding), - "dedupe_key": finding.get("dedupe_key") or finding.get("finding_id") or None, + "source_platform": source_platform, + "correlation_type": correlation_type, + "detection_layer": detection_layer, "detected_at": finding.get("first_seen") or finding.get("detected_at") or finding.get("created_at"), "first_seen_at": finding.get("first_seen") or finding.get("detected_at") or finding.get("created_at"), "last_seen_at": finding.get("last_seen") or finding.get("updated_at") or finding.get("created_at"), - "rule_id": finding.get("rule_id"), - "rule_name": finding.get("rule_name"), + "rule_id": finding.get("rule_id") or "unknown", + "rule_name": finding.get("rule_name") or finding.get("title") or "Unknown detector", "mitre": finding.get("mitre"), + "mitre_ids": to_jsonable(mitre_ids or []), "event_count": finding.get("event_count"), "event_ids": to_jsonable(finding.get("event_ids") or []), + "risk_tags": to_jsonable(risk_tags), "recommended_actions": to_jsonable(finding.get("recommended_actions") or []), "raw_payload": to_jsonable(finding), + "metadata": to_jsonable(metadata), } return row diff --git a/secopsai/cli.py b/secopsai/cli.py index 7853334..bb54ac0 100644 --- a/secopsai/cli.py +++ b/secopsai/cli.py @@ -214,7 +214,7 @@ def _run_correlate(time_window: int = 60, json_output: bool = False) -> int: print(f"Time window: {time_window} minutes") print(f"Total findings: {len(findings)}") - results = run_correlation(findings, time_window) + results = run_correlation(findings, time_window_minutes=time_window) if results["total_correlations"] > 0: message = f"""🚨 SecOpsAI Cross-Platform Alert diff --git a/secopsai/intel.py b/secopsai/intel.py index 218f3cd..d2f1151 100644 --- a/secopsai/intel.py +++ b/secopsai/intel.py @@ -39,6 +39,46 @@ DEFAULT_TIMEOUT_SECONDS = 20 +def _curated_supply_chain_iocs() -> List["IOC"]: + first_seen = "2026-03-31T00:00:00Z" + entries = [ + ("domain", "sfrclak.com", "elastic-curated", ["axios-compromise", "c2", "supply-chain"], 95), + ("ip", "142.11.206.73", "elastic-curated", ["axios-compromise", "c2", "supply-chain"], 95), + ("url", "http://sfrclak.com:8000/6202033", "elastic-curated", ["axios-compromise", "stage2", "supply-chain"], 98), + ("hash", "e10b1fa84f1d6481625f741b69892780140d4e0e7769e7491e5f4d894c2e0e09", "elastic-curated", ["axios-compromise", "setup.js", "supply-chain"], 95), + ("hash", "6483c004e207137385f480909d6edecf1b699087378aa91745ecba7c3394f9d7", "elastic-curated", ["axios-compromise", "linux-rat", "supply-chain"], 95), + ("hash", "ed8560c1ac7ceb6983ba995124d5917dc1a00288912387a6389296637d5f815c", "elastic-curated", ["axios-compromise", "powershell-rat", "supply-chain"], 95), + ("hash", "e49c2732fb9861548208a78e72996b9c3c470b6b562576924bcc3a9fb75bf9ff", "elastic-curated", ["axios-compromise", "persistence", "supply-chain"], 92), + ("hash", "92ff08773995ebc8d55ec4b8e1a225d0d1e51efa4ef88b8849d0071230c9645a", "elastic-curated", ["axios-compromise", "macos-backdoor", "supply-chain"], 95), + ("artifact", "axios@1.14.1", "elastic-curated", ["axios-compromise", "package-version", "supply-chain"], 88), + ("artifact", "axios@0.30.4", "elastic-curated", ["axios-compromise", "package-version", "supply-chain"], 88), + ("artifact", "plain-crypto-js@4.2.1", "elastic-curated", ["axios-compromise", "dependency", "postinstall"], 92), + ("artifact", "@shadanai/openclaw@2026.3.28-2", "elastic-curated", ["openclaw-ecosystem", "package-version", "supply-chain"], 90), + ("artifact", "@shadanai/openclaw@2026.3.28-3", "elastic-curated", ["openclaw-ecosystem", "package-version", "supply-chain"], 90), + ("artifact", "@shadanai/openclaw@2026.3.31-1", "elastic-curated", ["openclaw-ecosystem", "package-version", "supply-chain"], 90), + ("artifact", "@shadanai/openclaw@2026.3.31-2", "elastic-curated", ["openclaw-ecosystem", "package-version", "supply-chain"], 90), + ("artifact", "node_modules/plain-crypto-js/setup.js", "elastic-curated", ["axios-compromise", "filesystem", "postinstall"], 85), + ("artifact", "/tmp/ld.py", "elastic-curated", ["axios-compromise", "filesystem", "linux"], 90), + ("artifact", "programdata\\wt.exe", "elastic-curated", ["axios-compromise", "filesystem", "windows"], 90), + ("artifact", "programdata\\system.bat", "elastic-curated", ["axios-compromise", "filesystem", "windows"], 88), + ("artifact", "currentversion\\run\\microsoftupdate", "elastic-curated", ["axios-compromise", "registry", "persistence"], 92), + ("artifact", "/Library/Caches/com.apple.act.mond", "elastic-curated", ["axios-compromise", "filesystem", "macos"], 92), + ("artifact", ".scpt", "elastic-curated", ["axios-compromise", "applescript", "macos"], 82), + ] + return [ + IOC( + ioc_type=ioc_type, + value=value, + source=source, + tags=tags, + first_seen=first_seen, + last_seen=first_seen, + score=score, + ) + for ioc_type, value, source, tags, score in entries + ] + + def utc_now() -> str: return datetime.now(timezone.utc).isoformat().replace("+00:00", "Z") @@ -223,7 +263,7 @@ def refresh_iocs(*, timeout: int = DEFAULT_TIMEOUT_SECONDS) -> Dict[str, Any]: "threatfox": "https://threatfox.abuse.ch/export/csv/recent/", } - all_iocs: List[IOC] = [] + all_iocs: List[IOC] = _curated_supply_chain_iocs() errors: Dict[str, str] = {} for name, url in feeds.items(): @@ -253,7 +293,7 @@ def refresh_iocs(*, timeout: int = DEFAULT_TIMEOUT_SECONDS) -> Dict[str, Any]: payload = { "generated_at": utc_now(), "total": len(persisted), - "feeds": list(feeds.keys()), + "feeds": ["elastic-curated", *list(feeds.keys())], "errors": errors, "iocs": persisted, } diff --git a/tests/test_correlation_regression.py b/tests/test_correlation_regression.py new file mode 100644 index 0000000..258e749 --- /dev/null +++ b/tests/test_correlation_regression.py @@ -0,0 +1,37 @@ +import unittest +from pathlib import Path +import sys + + +REPO_ROOT = Path(__file__).resolve().parents[1] +if str(REPO_ROOT) not in sys.path: + sys.path.insert(0, str(REPO_ROOT)) + +from correlation import run_correlation + + +class CorrelationRegressionTests(unittest.TestCase): + def test_run_correlation_accepts_time_window_parameter(self): + findings = [ + { + "id": "f1", + "timestamp": "2026-04-11T06:00:00Z", + "platform": "openclaw", + "severity": "low", + "user": "alice", + }, + { + "id": "f2", + "timestamp": "2026-04-11T06:05:00Z", + "platform": "macos", + "severity": "low", + "user": "alice", + }, + ] + result = run_correlation(findings, time_window_minutes=30) + self.assertIn("total_correlations", result) + self.assertIsInstance(result["total_correlations"], int) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_regression.py b/tests/test_regression.py index d3cc558..551a4a3 100644 --- a/tests/test_regression.py +++ b/tests/test_regression.py @@ -20,8 +20,17 @@ class DetectionRegressionTests(unittest.TestCase): @classmethod def setUpClass(cls): data_dir = REPO_ROOT / "data" - cls.labeled_events = json.loads((data_dir / "events.json").read_text(encoding="utf-8")) - cls.unlabeled_events = json.loads((data_dir / "events_unlabeled.json").read_text(encoding="utf-8")) + fixtures_dir = data_dir / "fixtures" + labeled_path = data_dir / "events.json" + unlabeled_path = data_dir / "events_unlabeled.json" + + if not labeled_path.exists(): + labeled_path = fixtures_dir / "events.json" + if not unlabeled_path.exists(): + unlabeled_path = fixtures_dir / "events_unlabeled.json" + + cls.labeled_events = json.loads(labeled_path.read_text(encoding="utf-8")) + cls.unlabeled_events = json.loads(unlabeled_path.read_text(encoding="utf-8")) def test_detection_matches_benchmark_dataset(self): results = run_detection(self.unlabeled_events) @@ -135,4 +144,4 @@ def test_publish_to_github_issue_uses_gh_cli_without_overriding_env(self): if __name__ == "__main__": - unittest.main() \ No newline at end of file + unittest.main()