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