From 846256d7e51ed07d841bb249adf88fae59d6538a Mon Sep 17 00:00:00 2001 From: "Ioannis L." <44038245+blitzcrieg1@users.noreply.github.com> Date: Sun, 23 Aug 2026 12:16:42 +0300 Subject: [PATCH] feat(sigma): ship the fifteen sequence detections as Sigma rules The Sigma pack covered the recorder's own health: heartbeat silence, degraded coverage, MCP schema drift, denial bursts, untriaged criticals. All useful and none of it the product. The fifteen sequence detections, which are the thing a SOC would route on, had no Sigma representation at all, so a Splunk or Sentinel team wanting to alert on credential-exfil had to write the search themselves from a schema document. That is a strange gap for a project whose pitch is that your SIEM stays the console. If the console is theirs, the content has to be portable. Generated rather than written. Hand-writing fifteen would put rule ids, severities and MITRE ids in a second place that drifts from the first, which is the failure this repository keeps finding in its own documents. The generator replays the benchmark corpus through the real engine and reads the Detection objects it emits, so a severity that changes in rules.py changes here on the next run. Sixteen files for fifteen rule ids, and the extra one is a real finding. encoded-command-download emits two severities on purpose: critical for remote code fetched and executed, low for local content piped into an interpreter, with a comment in rules.py saying the low one exists "so it stops drowning the criticals". Keying the pack on rule_id alone produced a single rule at whichever severity the corpus happened to yield first, so it claimed critical while some firings are low. A test caught it. Each severity now gets its own rule with the selection pinned on action.outcome, because one static level would either page on the quiet variant or stay silent on the loud one. Rule UUIDs are derived from the rule id rather than generated, so rerunning is idempotent and a consumer who pinned an id keeps working. Severity joins the seed only for a split rule, which keeps the fourteen unsplit ids where they are. Five tests: every built-in rule has a Sigma rule, no Sigma rule outlives a deleted one, severities match what the engine emits, the documents are valid Sigma, and the ids do not churn on regeneration. Two of the fifteen have no corpus case and carry an explicit entry in the generator with the reason. host-subagent-swarm-burst needs several sessions on one host; off-hours-activity is opt-in behind an env var and a window. The table is checked against BUILTIN_RULE_IDS rather than trusted, so a sixteenth rule with neither a corpus case nor an entry fails the script instead of silently shipping a pack that claims more than it has. Tracked in #36. CI now lints tools/ as well, since the generator lives there and emits rules a SOC routes on. That surfaced one pre-existing S101 in mcp_audit_proxy.py, a type-narrowing assert after create_subprocess_exec, annotated rather than rewritten. 1125 tests, ruff clean, ruleset fingerprint unchanged at 56ad3de1 so the dogfood clock is untouched. Co-Authored-By: Claude Opus 5 --- .github/workflows/ci.yml | 4 +- apps/orchestrator/tests/test_sigma_pack.py | 126 +++++++++++ .../orchestrator/tools/generate_sigma_pack.py | 198 ++++++++++++++++++ apps/orchestrator/tools/mcp_audit_proxy.py | 5 +- docs/integrations/sigma/README.md | 54 +++++ ...try_rule_approval_denied_then_executed.yml | 28 +++ ...metry_rule_autonomous_unapproved_write.yml | 29 +++ .../agentmetry_rule_credential_exfil.yml | 31 +++ ...ry_rule_credential_read_then_cloud_api.yml | 31 +++ ...entmetry_rule_destructive_delete_burst.yml | 29 +++ ...agentmetry_rule_discovery_then_collect.yml | 31 +++ ...tmetry_rule_dotfile_read_then_git_push.yml | 31 +++ ...rule_encoded_command_download_critical.yml | 33 +++ ...etry_rule_encoded_command_download_low.yml | 31 +++ ...ntmetry_rule_host_subagent_swarm_burst.yml | 30 +++ .../agentmetry_rule_off_hours_activity.yml | 30 +++ ...entmetry_rule_pr_merged_without_review.yml | 29 +++ ...metry_rule_remote_staging_then_execute.yml | 31 +++ .../agentmetry_rule_session_tool_burst.yml | 29 +++ .../agentmetry_rule_subagent_swarm_burst.yml | 29 +++ ...rule_untrusted_input_then_risky_action.yml | 29 +++ 21 files changed, 866 insertions(+), 2 deletions(-) create mode 100644 apps/orchestrator/tests/test_sigma_pack.py create mode 100644 apps/orchestrator/tools/generate_sigma_pack.py create mode 100644 docs/integrations/sigma/agentmetry_rule_approval_denied_then_executed.yml create mode 100644 docs/integrations/sigma/agentmetry_rule_autonomous_unapproved_write.yml create mode 100644 docs/integrations/sigma/agentmetry_rule_credential_exfil.yml create mode 100644 docs/integrations/sigma/agentmetry_rule_credential_read_then_cloud_api.yml create mode 100644 docs/integrations/sigma/agentmetry_rule_destructive_delete_burst.yml create mode 100644 docs/integrations/sigma/agentmetry_rule_discovery_then_collect.yml create mode 100644 docs/integrations/sigma/agentmetry_rule_dotfile_read_then_git_push.yml create mode 100644 docs/integrations/sigma/agentmetry_rule_encoded_command_download_critical.yml create mode 100644 docs/integrations/sigma/agentmetry_rule_encoded_command_download_low.yml create mode 100644 docs/integrations/sigma/agentmetry_rule_host_subagent_swarm_burst.yml create mode 100644 docs/integrations/sigma/agentmetry_rule_off_hours_activity.yml create mode 100644 docs/integrations/sigma/agentmetry_rule_pr_merged_without_review.yml create mode 100644 docs/integrations/sigma/agentmetry_rule_remote_staging_then_execute.yml create mode 100644 docs/integrations/sigma/agentmetry_rule_session_tool_burst.yml create mode 100644 docs/integrations/sigma/agentmetry_rule_subagent_swarm_burst.yml create mode 100644 docs/integrations/sigma/agentmetry_rule_untrusted_input_then_risky_action.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 595505a..b9d5765 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -56,7 +56,9 @@ jobs: - name: Install orchestrator run: pip install -e ".[dev]" - name: Lint - run: ruff check agentmetry tests + # tools/ is linted too: the Sigma pack generator lives there and + # emits rules a SOC routes on, which is not a place for unlinted code. + run: ruff check agentmetry tests tools - name: Secret scan # The gitleaks action ships a Linux container only. if: matrix.os == 'ubuntu-latest' diff --git a/apps/orchestrator/tests/test_sigma_pack.py b/apps/orchestrator/tests/test_sigma_pack.py new file mode 100644 index 0000000..66ab024 --- /dev/null +++ b/apps/orchestrator/tests/test_sigma_pack.py @@ -0,0 +1,126 @@ +"""The Sigma pack ships severities and MITRE ids. Those rot. + +`docs/integrations/sigma/agentmetry_rule_*.yml` is generated by +`tools/generate_sigma_pack.py` from the real rule engine. A SOC that imports the +pack routes on the `level` and tags in these files, so a severity that changes +in `rules.py` and not here sends a critical to a low-priority queue and nobody +finds out until an incident. + +`test_readme_claims.py` already solves this shape for the corpus counts. This +does the same for the rules a customer actually alerts on. +""" + +from __future__ import annotations + +import re +from pathlib import Path + +import pytest +import yaml + +from agentmetry.core.audit.detection.rules import BUILTIN_RULE_IDS + +SIGMA_DIR = Path(__file__).resolve().parents[3] / "docs" / "integrations" / "sigma" + + +def _generated() -> dict[tuple[str, str], dict]: + """Keyed on (rule_id, level). + + One rule id is deliberately two detections: `encoded-command-download` is + critical for remote code fetched and executed and low for local content + piped into an interpreter. Those get one Sigma rule each, pinned on + `action.outcome`, so keying on rule_id alone would silently drop one. + """ + if not SIGMA_DIR.is_dir(): + pytest.skip(f"sigma pack not found at {SIGMA_DIR}") + out = {} + for path in SIGMA_DIR.glob("agentmetry_rule_*.yml"): + doc = yaml.safe_load(path.read_text(encoding="utf-8")) + out[(doc["detection"]["selection"]["detection.rule_id"], doc["level"])] = doc + return out + + +def _rule_ids(generated) -> set[str]: + return {rid for rid, _ in generated} + + +def test_every_builtin_rule_has_a_sigma_rule(): + """A rule with no Sigma rule is invisible to every SIEM that imports the pack.""" + generated = _generated() + missing = sorted(BUILTIN_RULE_IDS - _rule_ids(generated)) + assert not missing, ( + f"no Sigma rule for: {missing}. Run tools/generate_sigma_pack.py" + ) + + +def test_no_sigma_rule_for_a_rule_that_does_not_exist(): + """The reverse: a deleted rule leaving a Sigma file that never fires.""" + generated = _generated() + orphans = sorted(_rule_ids(generated) - BUILTIN_RULE_IDS) + assert not orphans, ( + f"Sigma rules for ids the engine does not emit: {orphans}. " + "Delete the file, or the pack promises a detection nobody produces." + ) + + +def test_severities_match_the_engine(): + """The number a SOC routes on has to be the number the engine emits. + + Harvested the same way the generator harvests it, so this fails when the + generator has not been re-run after a severity change. + """ + import json + + from agentmetry.core.audit.detection.benchmark import DEFAULT_CORPUS_DIR + from agentmetry.core.audit.detection.engine import run_detections, run_host_detections + + generated = _generated() + for path in sorted(Path(DEFAULT_CORPUS_DIR).glob("*.jsonl")): + events = [ + json.loads(line) + for line in path.read_text(encoding="utf-8").splitlines() + if line.strip() + ] + for det in (*run_detections(events), *run_host_detections(events)): + doc = generated.get((det.rule_id, det.severity)) + assert doc is not None, ( + f"{det.rule_id} fires at {det.severity} and no Sigma rule carries that " + "level. Run tools/generate_sigma_pack.py" + ) + + +def test_every_rule_is_valid_sigma(): + """Enough of the Sigma shape that an import does not fail on our side.""" + for (rule_id, _level), doc in _generated().items(): + assert doc.get("title", "").startswith("Agentmetry - "), rule_id + assert doc.get("logsource", {}).get("product") == "agentmetry", rule_id + assert doc.get("condition") is None, f"{rule_id}: condition belongs under detection" + assert doc["detection"]["condition"] == "selection", rule_id + assert doc.get("level") in {"critical", "high", "medium", "low"}, rule_id + # Sigma requires a UUID and consumers key on it. + assert re.fullmatch( + r"[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}", + str(doc.get("id", "")), + ), f"{rule_id}: id is not a UUID" + + +def test_rule_ids_are_stable_across_regeneration(): + """Regenerating must not churn every UUID. + + The ids are derived from the rule id rather than randomly, so a consumer who + pinned a Sigma id keeps working after somebody reruns the generator. + """ + import hashlib + import uuid + + generated = _generated() + counts = {} + for rid, _ in generated: + counts[rid] = counts.get(rid, 0) + 1 + for (rule_id, level), doc in generated.items(): + # Severity joins the seed only for a split rule, so unsplit ids never move. + seed = f"agentmetry.sigma.{rule_id}" + (f".{level}" if counts[rule_id] > 1 else "") + expected = str(uuid.UUID(bytes=hashlib.sha256(seed.encode()).digest()[:16], version=5)) + assert doc["id"] == expected, ( + f"{rule_id}: id is not the derived one. Regenerating would change it." + ) diff --git a/apps/orchestrator/tools/generate_sigma_pack.py b/apps/orchestrator/tools/generate_sigma_pack.py new file mode 100644 index 0000000..505806d --- /dev/null +++ b/apps/orchestrator/tools/generate_sigma_pack.py @@ -0,0 +1,198 @@ +"""Emit a Sigma rule per built-in sequence detection, into docs/integrations/sigma/. + +Run from the orchestrator: + + .venv/Scripts/python.exe tools/generate_sigma_pack.py + +## Why generated rather than written + +The Sigma pack that already existed covers the recorder's own health: heartbeat +silence, degraded coverage, MCP schema drift, denial bursts. Useful, and none of +it is the product. The fifteen sequence detections, which are what a SOC would +actually route on, had no Sigma representation at all, so a Splunk or Sentinel +team wanting to alert on `credential-exfil` had to write the search themselves +from a schema document. + +Hand-writing fifteen would put rule ids, severities and MITRE ids in a second +place that drifts from the first. So the metadata is harvested by replaying the +benchmark corpus through the real engine and reading the `Detection` objects it +emits. A severity that changes in `rules.py` changes here on the next run, and +`tests/test_sigma_pack.py` fails if somebody forgets to run it. + +## The two the corpus cannot produce + +`host-subagent-swarm-burst` is host-scoped and needs several sessions on one +host inside a window. `off-hours-activity` is opt-in behind +`AGENTMETRY_DETECT_OFF_HOURS` and needs an operator-set window. Neither has a +corpus case (issue #36), so both carry an explicit entry below. + +That table is checked against `BUILTIN_RULE_IDS` rather than trusted: adding a +sixteenth rule without either a corpus case or an entry here fails this script, +rather than silently shipping fourteen rules in a pack that claims fifteen. +""" + +from __future__ import annotations + +import hashlib +import json +import sys +import uuid +from pathlib import Path + +from agentmetry.core.audit.detection.benchmark import DEFAULT_CORPUS_DIR +from agentmetry.core.audit.detection.engine import run_detections, run_host_detections +from agentmetry.core.audit.detection.rules import BUILTIN_RULE_IDS + +OUT = Path(__file__).resolve().parents[2].parent / "docs" / "integrations" / "sigma" + +# Sigma's `level` vocabulary happens to match ours one for one. Mapped +# explicitly anyway, so a new severity fails here rather than emitting a level +# no SIEM recognises. +_LEVEL = {"critical": "critical", "high": "high", "medium": "medium", "low": "low"} + +# The two rules no corpus case can produce. Severity comes from +# docs/detection-rules.md, the same table the README renders. +_UNCOVERED: dict[str, dict] = { + "host-subagent-swarm-burst": { + "title": "Subagent swarm across sessions on one host", + "severity": "high", + "tactics": ["TA0002"], + "techniques": ["T1059"], + "note": "Host-scoped: needs several sessions on one host inside a window.", + }, + "off-hours-activity": { + "title": "Autonomous impact action outside business hours", + "severity": "medium", + "tactics": ["TA0040"], + "techniques": ["T1485"], + "note": "Opt-in behind AGENTMETRY_DETECT_OFF_HOURS with an operator-set window.", + }, +} + + +def _harvest() -> dict[tuple[str, str], dict]: + """Replay the corpus and read the Detection objects the engine emits. + + Keyed on `(rule_id, severity)` rather than `rule_id`, because one rule id is + deliberately two detections. `encoded-command-download` returns `critical` + for remote code fetched and executed and `low` for local content piped into + an interpreter, with a comment in `rules.py` saying the low one exists "so + it stops drowning the criticals". A single Sigma `level` would either page + on the quiet variant or stay silent on the loud one, so each severity gets + its own rule and the selection pins `action.outcome`. + """ + found: dict[tuple[str, str], dict] = {} + for path in sorted(Path(DEFAULT_CORPUS_DIR).glob("*.jsonl")): + events = [ + json.loads(line) + for line in path.read_text(encoding="utf-8").splitlines() + if line.strip() + ] + for det in (*run_detections(events), *run_host_detections(events)): + found.setdefault( + (det.rule_id, det.severity), + { + "title": det.title, + "severity": det.severity, + "tactics": sorted(set(det.tactic_ids)), + "techniques": sorted(set(det.technique_ids)), + "note": "", + }, + ) + return found + + +def _stable_uuid(rule_id: str, severity: str = "") -> str: + """A deterministic UUID, so regenerating does not churn every id. + + Sigma requires a UUID and consumers key on it, so running this twice must + produce no diff. Severity joins the seed only for a split rule, which keeps + the fourteen unsplit ids exactly where they are. + """ + seed = f"agentmetry.sigma.{rule_id}" + (f".{severity}" if severity else "") + return str(uuid.UUID(bytes=hashlib.sha256(seed.encode()).digest()[:16], version=5)) + + +def _yaml(rule_id: str, meta: dict, *, split: bool) -> str: + severity = meta["severity"] + tags = [f"attack.{t.lower()}" for t in meta["tactics"] + meta["techniques"]] + tag_lines = "\n".join(f" - {t}" for t in tags) or " []" + note = f"\n {meta['note']}" if meta.get("note") else "" + outcome = f"\n action.outcome: {severity}" if split else "" + variant = ( + f"\n This rule id emits more than one severity. This is the `{severity}`" + " variant, pinned on `action.outcome`." + if split + else "" + ) + return f"""title: Agentmetry - {meta["title"]} +id: {_stable_uuid(rule_id, severity if split else "")} +status: experimental +description: | + Agentmetry sequence detection `{rule_id}` fired. The individual tool calls are + ordinary; the order is the finding, and `detection.event_ids` names the exact + events that produced it.{note}{variant} + + GENERATED by tools/generate_sigma_pack.py. Do not hand-edit: severity and + MITRE ids are read from the rule engine, so an edit here is a claim that + disagrees with the code. +references: + - https://github.com/blitzcrieg1/agentmetry/blob/master/docs/detection-rules.md +author: Agentmetry +logsource: + product: agentmetry + service: audit +detection: + selection: + action.type: detection + detection.rule_id: {rule_id}{outcome} + condition: selection +falsepositives: + - Legitimate automation that reproduces the sequence. Disposition it in the + trail rather than deleting the rule, so the decision is recorded. +level: {_LEVEL[severity]} +tags: +{tag_lines} +""" + + +def main() -> int: + harvested = _harvest() + for rule_id, entry in _UNCOVERED.items(): + harvested.setdefault((rule_id, entry["severity"]), entry) + + covered = {rid for rid, _ in harvested} + missing = sorted(BUILTIN_RULE_IDS - covered) + if missing: + print("No corpus case and no _UNCOVERED entry for: " + ", ".join(missing), file=sys.stderr) + print("Add a corpus case (preferred, see issue #36) or an entry here.", file=sys.stderr) + return 1 + + extra = sorted(covered - BUILTIN_RULE_IDS) + if extra: + # A rule that fired in the corpus but is not built in means a YAML rule + # leaked into the run, which would ship an operator's local rule as ours. + print("Not a built-in rule: " + ", ".join(extra), file=sys.stderr) + return 1 + + severities: dict[str, set[str]] = {} + for rid, sev in harvested: + severities.setdefault(rid, set()).add(sev) + + OUT.mkdir(parents=True, exist_ok=True) + for rule_id, severity in sorted(harvested): + split = len(severities[rule_id]) > 1 + stem = rule_id.replace("-", "_") + (f"_{severity}" if split else "") + (OUT / f"agentmetry_rule_{stem}.yml").write_text( + _yaml(rule_id, harvested[(rule_id, severity)], split=split), encoding="utf-8" + ) + + split_ids = sorted(r for r, s in severities.items() if len(s) > 1) + print(f"wrote {len(harvested)} Sigma rules for {len(BUILTIN_RULE_IDS)} rule ids -> {OUT}") + if split_ids: + print(f" split by severity: {', '.join(split_ids)}") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/apps/orchestrator/tools/mcp_audit_proxy.py b/apps/orchestrator/tools/mcp_audit_proxy.py index 32d0fea..8f9d30d 100644 --- a/apps/orchestrator/tools/mcp_audit_proxy.py +++ b/apps/orchestrator/tools/mcp_audit_proxy.py @@ -204,7 +204,10 @@ async def run_proxy(command: list[str], server_name: str) -> int: stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE, ) - assert proc.stdin and proc.stdout and proc.stderr + # noqa S101: narrowing for the type checker, not a runtime check. + # create_subprocess_exec with PIPE on all three always sets them, and + # python -O would strip this without changing behaviour. + assert proc.stdin and proc.stdout and proc.stderr # noqa: S101 pending: dict[str, dict[str, str]] = {} list_buf = ToolsListBuffer() diff --git a/docs/integrations/sigma/README.md b/docs/integrations/sigma/README.md index c153ef5..71b6d1c 100644 --- a/docs/integrations/sigma/README.md +++ b/docs/integrations/sigma/README.md @@ -13,6 +13,60 @@ Portable [Sigma](https://sigmahq.io/) rules for Agentmetry canonical events, por | Recorder silent | `agentmetry_recorder_silent.yml` | informational | The heartbeat itself. Not a finding alone; the correlation notes in the file carry the absence check, including a machine that never enrolled | | MCP tool schema changed | `agentmetry_mcp_schema_changed.yml` | medium | `tools/list` fingerprint moved. Rug pull / tool poisoning. Config digest may be unchanged; that conjunction is in the rule file | +--- + +## The sequence detections (generated) + +The table above covers the recorder's **own health**: is it alive, is coverage +intact, did an MCP schema move, is anybody triaging. Useful, and none of it is +the product. + +`agentmetry_rule_*.yml` covers the fifteen **sequence detections**, which are +what a SOC actually routes on. One file per rule, matching the detection event +in the trail: + +```yaml +detection: + selection: + action.type: detection + detection.rule_id: credential-exfil + condition: selection +level: critical +tags: + - attack.ta0006 + - attack.ta0011 + - attack.t1071.001 + - attack.t1552.004 +``` + +Sixteen files for fifteen rules. `encoded-command-download` is deliberately two +detections: `critical` for remote code fetched and executed, `low` for local +content piped into an interpreter, where `rules.py` says the low one exists "so +it stops drowning the criticals". A single Sigma `level` would either page on +the quiet variant or stay silent on the loud one, so each gets a rule and the +selection pins `action.outcome`. + +### Do not hand-edit these + +They are produced by `apps/orchestrator/tools/generate_sigma_pack.py`, which +replays the benchmark corpus through the real rule engine and reads the +severities and MITRE ids off the `Detection` objects it emits. Editing a level +here creates a claim that disagrees with the code, and the code is what fires. + +```bash +cd apps/orchestrator +.venv/Scripts/python.exe tools/generate_sigma_pack.py +``` + +Rerunning is idempotent: the rule UUIDs are derived from the rule id rather than +generated, so a consumer who pinned one keeps working. + +`tests/test_sigma_pack.py` fails if a rule is added without a Sigma rule, if a +Sigma rule survives a deleted rule, or if a severity here disagrees with the +engine. Two of the fifteen (`host-subagent-swarm-burst`, `off-hours-activity`) +have no corpus case and carry an explicit entry in the generator, which is +tracked in [issue #36](https://github.com/blitzcrieg1/agentmetry/issues/36). + ## Field mapping — read before deploying Sigma rules reference the **canonical schema** ([`../../agentmetry-event-schema.md`](../../agentmetry-event-schema.md)) using dotted JSON paths (`action.type`, `tool.qualified`). **Field names differ by the sink you forward to** — you must supply the right Sigma processing pipeline for your backend, or adjust the field names: diff --git a/docs/integrations/sigma/agentmetry_rule_approval_denied_then_executed.yml b/docs/integrations/sigma/agentmetry_rule_approval_denied_then_executed.yml new file mode 100644 index 0000000..c49a2da --- /dev/null +++ b/docs/integrations/sigma/agentmetry_rule_approval_denied_then_executed.yml @@ -0,0 +1,28 @@ +title: Agentmetry - Denied action was executed +id: 41301061-e851-5622-91e0-1b5155bedc32 +status: experimental +description: | + Agentmetry sequence detection `approval-denied-then-executed` fired. The individual tool calls are + ordinary; the order is the finding, and `detection.event_ids` names the exact + events that produced it. + + GENERATED by tools/generate_sigma_pack.py. Do not hand-edit: severity and + MITRE ids are read from the rule engine, so an edit here is a claim that + disagrees with the code. +references: + - https://github.com/blitzcrieg1/agentmetry/blob/master/docs/detection-rules.md +author: Agentmetry +logsource: + product: agentmetry + service: audit +detection: + selection: + action.type: detection + detection.rule_id: approval-denied-then-executed + condition: selection +falsepositives: + - Legitimate automation that reproduces the sequence. Disposition it in the + trail rather than deleting the rule, so the decision is recorded. +level: critical +tags: + - attack.ta0005 diff --git a/docs/integrations/sigma/agentmetry_rule_autonomous_unapproved_write.yml b/docs/integrations/sigma/agentmetry_rule_autonomous_unapproved_write.yml new file mode 100644 index 0000000..43fd61b --- /dev/null +++ b/docs/integrations/sigma/agentmetry_rule_autonomous_unapproved_write.yml @@ -0,0 +1,29 @@ +title: Agentmetry - Autonomous write without human approval +id: 6b628d9f-f115-5356-8580-1d181d788945 +status: experimental +description: | + Agentmetry sequence detection `autonomous-unapproved-write` fired. The individual tool calls are + ordinary; the order is the finding, and `detection.event_ids` names the exact + events that produced it. + + GENERATED by tools/generate_sigma_pack.py. Do not hand-edit: severity and + MITRE ids are read from the rule engine, so an edit here is a claim that + disagrees with the code. +references: + - https://github.com/blitzcrieg1/agentmetry/blob/master/docs/detection-rules.md +author: Agentmetry +logsource: + product: agentmetry + service: audit +detection: + selection: + action.type: detection + detection.rule_id: autonomous-unapproved-write + condition: selection +falsepositives: + - Legitimate automation that reproduces the sequence. Disposition it in the + trail rather than deleting the rule, so the decision is recorded. +level: high +tags: + - attack.ta0040 + - attack.t1565 diff --git a/docs/integrations/sigma/agentmetry_rule_credential_exfil.yml b/docs/integrations/sigma/agentmetry_rule_credential_exfil.yml new file mode 100644 index 0000000..ed44955 --- /dev/null +++ b/docs/integrations/sigma/agentmetry_rule_credential_exfil.yml @@ -0,0 +1,31 @@ +title: Agentmetry - Credential access followed by network egress +id: d00e2692-5c58-5e78-89b4-1ff6ac685304 +status: experimental +description: | + Agentmetry sequence detection `credential-exfil` fired. The individual tool calls are + ordinary; the order is the finding, and `detection.event_ids` names the exact + events that produced it. + + GENERATED by tools/generate_sigma_pack.py. Do not hand-edit: severity and + MITRE ids are read from the rule engine, so an edit here is a claim that + disagrees with the code. +references: + - https://github.com/blitzcrieg1/agentmetry/blob/master/docs/detection-rules.md +author: Agentmetry +logsource: + product: agentmetry + service: audit +detection: + selection: + action.type: detection + detection.rule_id: credential-exfil + condition: selection +falsepositives: + - Legitimate automation that reproduces the sequence. Disposition it in the + trail rather than deleting the rule, so the decision is recorded. +level: critical +tags: + - attack.ta0006 + - attack.ta0011 + - attack.t1071.001 + - attack.t1552.004 diff --git a/docs/integrations/sigma/agentmetry_rule_credential_read_then_cloud_api.yml b/docs/integrations/sigma/agentmetry_rule_credential_read_then_cloud_api.yml new file mode 100644 index 0000000..3c3b0da --- /dev/null +++ b/docs/integrations/sigma/agentmetry_rule_credential_read_then_cloud_api.yml @@ -0,0 +1,31 @@ +title: Agentmetry - Credential access followed by cloud or cluster API +id: f09943f8-50b6-59cb-9349-450d1b655156 +status: experimental +description: | + Agentmetry sequence detection `credential-read-then-cloud-api` fired. The individual tool calls are + ordinary; the order is the finding, and `detection.event_ids` names the exact + events that produced it. + + GENERATED by tools/generate_sigma_pack.py. Do not hand-edit: severity and + MITRE ids are read from the rule engine, so an edit here is a claim that + disagrees with the code. +references: + - https://github.com/blitzcrieg1/agentmetry/blob/master/docs/detection-rules.md +author: Agentmetry +logsource: + product: agentmetry + service: audit +detection: + selection: + action.type: detection + detection.rule_id: credential-read-then-cloud-api + condition: selection +falsepositives: + - Legitimate automation that reproduces the sequence. Disposition it in the + trail rather than deleting the rule, so the decision is recorded. +level: critical +tags: + - attack.ta0006 + - attack.ta0008 + - attack.t1078 + - attack.t1552.004 diff --git a/docs/integrations/sigma/agentmetry_rule_destructive_delete_burst.yml b/docs/integrations/sigma/agentmetry_rule_destructive_delete_burst.yml new file mode 100644 index 0000000..592e9b3 --- /dev/null +++ b/docs/integrations/sigma/agentmetry_rule_destructive_delete_burst.yml @@ -0,0 +1,29 @@ +title: Agentmetry - Burst of destructive deletions +id: 35517af0-1dde-5ce1-aabe-f1d402427bf0 +status: experimental +description: | + Agentmetry sequence detection `destructive-delete-burst` fired. The individual tool calls are + ordinary; the order is the finding, and `detection.event_ids` names the exact + events that produced it. + + GENERATED by tools/generate_sigma_pack.py. Do not hand-edit: severity and + MITRE ids are read from the rule engine, so an edit here is a claim that + disagrees with the code. +references: + - https://github.com/blitzcrieg1/agentmetry/blob/master/docs/detection-rules.md +author: Agentmetry +logsource: + product: agentmetry + service: audit +detection: + selection: + action.type: detection + detection.rule_id: destructive-delete-burst + condition: selection +falsepositives: + - Legitimate automation that reproduces the sequence. Disposition it in the + trail rather than deleting the rule, so the decision is recorded. +level: high +tags: + - attack.ta0040 + - attack.t1485 diff --git a/docs/integrations/sigma/agentmetry_rule_discovery_then_collect.yml b/docs/integrations/sigma/agentmetry_rule_discovery_then_collect.yml new file mode 100644 index 0000000..e6b3e11 --- /dev/null +++ b/docs/integrations/sigma/agentmetry_rule_discovery_then_collect.yml @@ -0,0 +1,31 @@ +title: Agentmetry - Filesystem recon followed by data collection +id: 81984746-72c5-5284-babb-cb61bfaca83b +status: experimental +description: | + Agentmetry sequence detection `discovery-then-collect` fired. The individual tool calls are + ordinary; the order is the finding, and `detection.event_ids` names the exact + events that produced it. + + GENERATED by tools/generate_sigma_pack.py. Do not hand-edit: severity and + MITRE ids are read from the rule engine, so an edit here is a claim that + disagrees with the code. +references: + - https://github.com/blitzcrieg1/agentmetry/blob/master/docs/detection-rules.md +author: Agentmetry +logsource: + product: agentmetry + service: audit +detection: + selection: + action.type: detection + detection.rule_id: discovery-then-collect + condition: selection +falsepositives: + - Legitimate automation that reproduces the sequence. Disposition it in the + trail rather than deleting the rule, so the decision is recorded. +level: medium +tags: + - attack.ta0007 + - attack.ta0009 + - attack.t1005 + - attack.t1083 diff --git a/docs/integrations/sigma/agentmetry_rule_dotfile_read_then_git_push.yml b/docs/integrations/sigma/agentmetry_rule_dotfile_read_then_git_push.yml new file mode 100644 index 0000000..daca49a --- /dev/null +++ b/docs/integrations/sigma/agentmetry_rule_dotfile_read_then_git_push.yml @@ -0,0 +1,31 @@ +title: Agentmetry - Credential read followed by git push +id: ed49db45-92e7-5555-ba09-97361d0b8b01 +status: experimental +description: | + Agentmetry sequence detection `dotfile-read-then-git-push` fired. The individual tool calls are + ordinary; the order is the finding, and `detection.event_ids` names the exact + events that produced it. + + GENERATED by tools/generate_sigma_pack.py. Do not hand-edit: severity and + MITRE ids are read from the rule engine, so an edit here is a claim that + disagrees with the code. +references: + - https://github.com/blitzcrieg1/agentmetry/blob/master/docs/detection-rules.md +author: Agentmetry +logsource: + product: agentmetry + service: audit +detection: + selection: + action.type: detection + detection.rule_id: dotfile-read-then-git-push + condition: selection +falsepositives: + - Legitimate automation that reproduces the sequence. Disposition it in the + trail rather than deleting the rule, so the decision is recorded. +level: critical +tags: + - attack.ta0006 + - attack.ta0010 + - attack.t1552.001 + - attack.t1567.001 diff --git a/docs/integrations/sigma/agentmetry_rule_encoded_command_download_critical.yml b/docs/integrations/sigma/agentmetry_rule_encoded_command_download_critical.yml new file mode 100644 index 0000000..30d5324 --- /dev/null +++ b/docs/integrations/sigma/agentmetry_rule_encoded_command_download_critical.yml @@ -0,0 +1,33 @@ +title: Agentmetry - Remote code fetched and executed +id: bc51b9c3-e6c4-5738-9bf9-a95acd1266d6 +status: experimental +description: | + Agentmetry sequence detection `encoded-command-download` fired. The individual tool calls are + ordinary; the order is the finding, and `detection.event_ids` names the exact + events that produced it. + This rule id emits more than one severity. This is the `critical` variant, pinned on `action.outcome`. + + GENERATED by tools/generate_sigma_pack.py. Do not hand-edit: severity and + MITRE ids are read from the rule engine, so an edit here is a claim that + disagrees with the code. +references: + - https://github.com/blitzcrieg1/agentmetry/blob/master/docs/detection-rules.md +author: Agentmetry +logsource: + product: agentmetry + service: audit +detection: + selection: + action.type: detection + detection.rule_id: encoded-command-download + action.outcome: critical + condition: selection +falsepositives: + - Legitimate automation that reproduces the sequence. Disposition it in the + trail rather than deleting the rule, so the decision is recorded. +level: critical +tags: + - attack.ta0002 + - attack.ta0011 + - attack.t1059.001 + - attack.t1105 diff --git a/docs/integrations/sigma/agentmetry_rule_encoded_command_download_low.yml b/docs/integrations/sigma/agentmetry_rule_encoded_command_download_low.yml new file mode 100644 index 0000000..b939c5c --- /dev/null +++ b/docs/integrations/sigma/agentmetry_rule_encoded_command_download_low.yml @@ -0,0 +1,31 @@ +title: Agentmetry - Local content piped into an interpreter +id: 7ce5d9e8-bf17-513d-b06d-f33bf78e77ff +status: experimental +description: | + Agentmetry sequence detection `encoded-command-download` fired. The individual tool calls are + ordinary; the order is the finding, and `detection.event_ids` names the exact + events that produced it. + This rule id emits more than one severity. This is the `low` variant, pinned on `action.outcome`. + + GENERATED by tools/generate_sigma_pack.py. Do not hand-edit: severity and + MITRE ids are read from the rule engine, so an edit here is a claim that + disagrees with the code. +references: + - https://github.com/blitzcrieg1/agentmetry/blob/master/docs/detection-rules.md +author: Agentmetry +logsource: + product: agentmetry + service: audit +detection: + selection: + action.type: detection + detection.rule_id: encoded-command-download + action.outcome: low + condition: selection +falsepositives: + - Legitimate automation that reproduces the sequence. Disposition it in the + trail rather than deleting the rule, so the decision is recorded. +level: low +tags: + - attack.ta0002 + - attack.t1059 diff --git a/docs/integrations/sigma/agentmetry_rule_host_subagent_swarm_burst.yml b/docs/integrations/sigma/agentmetry_rule_host_subagent_swarm_burst.yml new file mode 100644 index 0000000..8813724 --- /dev/null +++ b/docs/integrations/sigma/agentmetry_rule_host_subagent_swarm_burst.yml @@ -0,0 +1,30 @@ +title: Agentmetry - Subagent swarm across sessions on one host +id: 21517c05-ef55-502e-a083-82c3f326a982 +status: experimental +description: | + Agentmetry sequence detection `host-subagent-swarm-burst` fired. The individual tool calls are + ordinary; the order is the finding, and `detection.event_ids` names the exact + events that produced it. + Host-scoped: needs several sessions on one host inside a window. + + GENERATED by tools/generate_sigma_pack.py. Do not hand-edit: severity and + MITRE ids are read from the rule engine, so an edit here is a claim that + disagrees with the code. +references: + - https://github.com/blitzcrieg1/agentmetry/blob/master/docs/detection-rules.md +author: Agentmetry +logsource: + product: agentmetry + service: audit +detection: + selection: + action.type: detection + detection.rule_id: host-subagent-swarm-burst + condition: selection +falsepositives: + - Legitimate automation that reproduces the sequence. Disposition it in the + trail rather than deleting the rule, so the decision is recorded. +level: high +tags: + - attack.ta0002 + - attack.t1059 diff --git a/docs/integrations/sigma/agentmetry_rule_off_hours_activity.yml b/docs/integrations/sigma/agentmetry_rule_off_hours_activity.yml new file mode 100644 index 0000000..9f32899 --- /dev/null +++ b/docs/integrations/sigma/agentmetry_rule_off_hours_activity.yml @@ -0,0 +1,30 @@ +title: Agentmetry - Autonomous impact action outside business hours +id: 240a50e2-329b-587f-98fb-9e9277ca3cde +status: experimental +description: | + Agentmetry sequence detection `off-hours-activity` fired. The individual tool calls are + ordinary; the order is the finding, and `detection.event_ids` names the exact + events that produced it. + Opt-in behind AGENTMETRY_DETECT_OFF_HOURS with an operator-set window. + + GENERATED by tools/generate_sigma_pack.py. Do not hand-edit: severity and + MITRE ids are read from the rule engine, so an edit here is a claim that + disagrees with the code. +references: + - https://github.com/blitzcrieg1/agentmetry/blob/master/docs/detection-rules.md +author: Agentmetry +logsource: + product: agentmetry + service: audit +detection: + selection: + action.type: detection + detection.rule_id: off-hours-activity + condition: selection +falsepositives: + - Legitimate automation that reproduces the sequence. Disposition it in the + trail rather than deleting the rule, so the decision is recorded. +level: medium +tags: + - attack.ta0040 + - attack.t1485 diff --git a/docs/integrations/sigma/agentmetry_rule_pr_merged_without_review.yml b/docs/integrations/sigma/agentmetry_rule_pr_merged_without_review.yml new file mode 100644 index 0000000..f7a761c --- /dev/null +++ b/docs/integrations/sigma/agentmetry_rule_pr_merged_without_review.yml @@ -0,0 +1,29 @@ +title: Agentmetry - Pull request merged without reading the code +id: 88c5fe48-9074-58c1-be67-55e69e10c146 +status: experimental +description: | + Agentmetry sequence detection `pr-merged-without-review` fired. The individual tool calls are + ordinary; the order is the finding, and `detection.event_ids` names the exact + events that produced it. + + GENERATED by tools/generate_sigma_pack.py. Do not hand-edit: severity and + MITRE ids are read from the rule engine, so an edit here is a claim that + disagrees with the code. +references: + - https://github.com/blitzcrieg1/agentmetry/blob/master/docs/detection-rules.md +author: Agentmetry +logsource: + product: agentmetry + service: audit +detection: + selection: + action.type: detection + detection.rule_id: pr-merged-without-review + condition: selection +falsepositives: + - Legitimate automation that reproduces the sequence. Disposition it in the + trail rather than deleting the rule, so the decision is recorded. +level: critical +tags: + - attack.ta0001 + - attack.t1195.002 diff --git a/docs/integrations/sigma/agentmetry_rule_remote_staging_then_execute.yml b/docs/integrations/sigma/agentmetry_rule_remote_staging_then_execute.yml new file mode 100644 index 0000000..c0627d3 --- /dev/null +++ b/docs/integrations/sigma/agentmetry_rule_remote_staging_then_execute.yml @@ -0,0 +1,31 @@ +title: Agentmetry - Downloaded file executed in the same session +id: ae9d8524-6bd9-55c0-940d-ceab58928e4c +status: experimental +description: | + Agentmetry sequence detection `remote-staging-then-execute` fired. The individual tool calls are + ordinary; the order is the finding, and `detection.event_ids` names the exact + events that produced it. + + GENERATED by tools/generate_sigma_pack.py. Do not hand-edit: severity and + MITRE ids are read from the rule engine, so an edit here is a claim that + disagrees with the code. +references: + - https://github.com/blitzcrieg1/agentmetry/blob/master/docs/detection-rules.md +author: Agentmetry +logsource: + product: agentmetry + service: audit +detection: + selection: + action.type: detection + detection.rule_id: remote-staging-then-execute + condition: selection +falsepositives: + - Legitimate automation that reproduces the sequence. Disposition it in the + trail rather than deleting the rule, so the decision is recorded. +level: critical +tags: + - attack.ta0002 + - attack.ta0011 + - attack.t1059 + - attack.t1105 diff --git a/docs/integrations/sigma/agentmetry_rule_session_tool_burst.yml b/docs/integrations/sigma/agentmetry_rule_session_tool_burst.yml new file mode 100644 index 0000000..a55cf96 --- /dev/null +++ b/docs/integrations/sigma/agentmetry_rule_session_tool_burst.yml @@ -0,0 +1,29 @@ +title: Agentmetry - Burst of tool calls in one session +id: c697e8e3-57df-5a06-82d0-29cb8d82f690 +status: experimental +description: | + Agentmetry sequence detection `session-tool-burst` fired. The individual tool calls are + ordinary; the order is the finding, and `detection.event_ids` names the exact + events that produced it. + + GENERATED by tools/generate_sigma_pack.py. Do not hand-edit: severity and + MITRE ids are read from the rule engine, so an edit here is a claim that + disagrees with the code. +references: + - https://github.com/blitzcrieg1/agentmetry/blob/master/docs/detection-rules.md +author: Agentmetry +logsource: + product: agentmetry + service: audit +detection: + selection: + action.type: detection + detection.rule_id: session-tool-burst + condition: selection +falsepositives: + - Legitimate automation that reproduces the sequence. Disposition it in the + trail rather than deleting the rule, so the decision is recorded. +level: high +tags: + - attack.ta0002 + - attack.t1059 diff --git a/docs/integrations/sigma/agentmetry_rule_subagent_swarm_burst.yml b/docs/integrations/sigma/agentmetry_rule_subagent_swarm_burst.yml new file mode 100644 index 0000000..bd35557 --- /dev/null +++ b/docs/integrations/sigma/agentmetry_rule_subagent_swarm_burst.yml @@ -0,0 +1,29 @@ +title: Agentmetry - Burst of subagent spawns in one session +id: 95c5b94c-ad23-5d6a-a53f-9a6009f750a0 +status: experimental +description: | + Agentmetry sequence detection `subagent-swarm-burst` fired. The individual tool calls are + ordinary; the order is the finding, and `detection.event_ids` names the exact + events that produced it. + + GENERATED by tools/generate_sigma_pack.py. Do not hand-edit: severity and + MITRE ids are read from the rule engine, so an edit here is a claim that + disagrees with the code. +references: + - https://github.com/blitzcrieg1/agentmetry/blob/master/docs/detection-rules.md +author: Agentmetry +logsource: + product: agentmetry + service: audit +detection: + selection: + action.type: detection + detection.rule_id: subagent-swarm-burst + condition: selection +falsepositives: + - Legitimate automation that reproduces the sequence. Disposition it in the + trail rather than deleting the rule, so the decision is recorded. +level: high +tags: + - attack.ta0002 + - attack.t1059 diff --git a/docs/integrations/sigma/agentmetry_rule_untrusted_input_then_risky_action.yml b/docs/integrations/sigma/agentmetry_rule_untrusted_input_then_risky_action.yml new file mode 100644 index 0000000..14bae8a --- /dev/null +++ b/docs/integrations/sigma/agentmetry_rule_untrusted_input_then_risky_action.yml @@ -0,0 +1,29 @@ +title: Agentmetry - Risky action after ingesting untrusted content +id: 97bfbab2-e292-5f20-adc6-fd0e3a500e27 +status: experimental +description: | + Agentmetry sequence detection `untrusted-input-then-risky-action` fired. The individual tool calls are + ordinary; the order is the finding, and `detection.event_ids` names the exact + events that produced it. + + GENERATED by tools/generate_sigma_pack.py. Do not hand-edit: severity and + MITRE ids are read from the rule engine, so an edit here is a claim that + disagrees with the code. +references: + - https://github.com/blitzcrieg1/agentmetry/blob/master/docs/detection-rules.md +author: Agentmetry +logsource: + product: agentmetry + service: audit +detection: + selection: + action.type: detection + detection.rule_id: untrusted-input-then-risky-action + condition: selection +falsepositives: + - Legitimate automation that reproduces the sequence. Disposition it in the + trail rather than deleting the rule, so the decision is recorded. +level: high +tags: + - attack.ta0011 + - attack.t1071.001