Skip to content

Deepen the MITRE resolution seam: one kill-chain resolver, one technique-ID form, one defence entry point - #114

Open
mrwadams wants to merge 2 commits into
mainfrom
agent/issue-111
Open

mrwadams wants to merge 2 commits into
mainfrom
agent/issue-111

Conversation

@mrwadams

@mrwadams mrwadams commented Sep 11, 2026

Copy link
Copy Markdown
Owner

Automated by agent-loop — the agent worked this issue on a fresh clone of mrwadams/attackgen inside a disposable sandbox; changes were gated outside the agent.

Closes #111

How to test

git fetch origin && git checkout agent/issue-111
python3 -m venv .venv && . .venv/bin/activate
pip install -r requirements.txt
streamlit run "00_👋_Welcome.py"

Then open the printed local URL.

Confirm each acceptance criterion:

  • One core.attack_data call resolves a kill chain for Enterprise, ICS and ATLAS; no caller outside core branches on the matrix to choose a resolver.
  • KillChain exposes its technique IDs; no caller re-derives them from its technique records.
  • One shared helper turns "Name (ID)" labels or bare IDs into bare IDs, and the Custom Scenarios page and the MCP techniques argument both use it, so the same selection normalises identically.
  • One core.detections call builds the Detection & Response report from a matrix and technique IDs, resolving the STIX bundle itself; no caller passes mitre_data or atlas_data.
  • build_defense_report still accepts injected mitre_data / atlas_data, and the existing detections and MCP tests keep using it unchanged.
  • The private _resolve_kill_chain, _normalise_ids and _defense_markdown helpers are gone from the MCP server.
  • New tests cover the matrix dispatch in both new entry points, including the ATLAS path and an unknown matrix.
  • pytest is green, and generated scenarios, Navigator layers and detection reports are unchanged for the same inputs.

Gates (non-authoritative)

  • files_non_empty — 11 non-empty file(s) changed
  • containment — diff stays within the allowed lane
  • verify — pytest: 350 passed, no failures (browser/e2e excluded)

Draft. Browser/e2e tests were NOT run in-gate — run them and review before merging. Nothing here is auto-merged.

…que-ID form, one defence entry point

Implements #111 (agent-loop).
@mrwadams mrwadams added the awaiting-verification Opened by agent-loop; not yet verified against a running app label Sep 11, 2026
Comment thread tests/test_detections.py Fixed
The resolver tests imported core.detections both as a module and via
from-import only to reach its `ad` alias, which CodeQL flags as
py/import-and-import-from. Patch the loaders on core.attack_data by
string target instead, matching the other test modules.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019MbUcFXnuA8VFEdeXeFh9q
@mrwadams

Copy link
Copy Markdown
Owner Author
{
  "schema": "pr-verify/1",
  "repo": "mrwadams/attackgen",
  "pr": 114,
  "head_sha": "4fc9c90b20589f7ad86ce12dea102b5d46b0a50f",
  "verdict": "verified",
  "verified_at": "2026-09-15T05:28:52Z",
  "verifier_commits": [],
  "criteria": [
    {
      "text": "One `core.attack_data` call resolves a kill chain for Enterprise, ICS and ATLAS; no caller outside `core` branches on the matrix to choose a resolver.",
      "state": "observed",
      "note": "Headless Chrome on a live `streamlit run`, Threat Group Scenarios page: APT28/Enterprise resolved to `13 techniques`, CyberAv3ngers/ICS to `3 techniques`, VirusTotal Poisoning/ATLAS to `4 techniques` (readiness line read off the rendered page each time). Over MCP stdio JSON-RPC, `get_kill_chain` returned a populated kill chain for all three matrices. Repo-wide grep: the only remaining `matrix == \"ATLAS\"` branches outside `core` are label/heading text (page 1 lines 113/156/177/193/240, page 2's technique-option listing), none selecting a resolver."
    },
    {
      "text": "`KillChain` exposes its technique IDs; no caller re-derives them from its technique records.",
      "state": "observed",
      "note": "Page 1's downloaded `..._detection.md` for APT28 carried exactly the 13 sampled techniques (T1598.003, T1584, T1566.001, T1204.001, T1547.001, T1546.015, T1557.004, T1057, T1550.002, T1213, T1071.003, T1048.002, T1561.001), matching the kill chain the page had just shown — the page now feeds `kill_chain.technique_ids` through the snapshot. Over MCP, `generate_threat_group_scenario(include_detection=true)` appended a Detection & Response section for both Enterprise and ATLAS via `kc.technique_ids`. Grep: no caller outside `core` rebuilds the ID list from `techniques`. (Page 1 line 69 still reads `row[\"ATT&CK ID\"]`, but to pair each ID with its tactic shortname for the Navigator layer — a shape `technique_ids` does not provide.)"
    },
    {
      "text": "One shared helper turns `\"Name (ID)\"` labels or bare IDs into bare IDs, and the Custom Scenarios page and the MCP `techniques` argument both use it, so the same selection normalises identically.",
      "state": "observed",
      "note": "Selected 6 techniques in the Custom Scenarios multiselect in the browser (T1059, T1566 and its four sub-techniques), downloaded the layer and detection report, then called MCP `get_navigator_layer` / `get_detection_report` with that same selection as display labels. The browser's `..._layer.json` is byte-identical to the MCP `layer_json`, and the detection report bodies are equal. Within MCP, passing labels vs bare IDs produced identical output for both tools, and `[\"...(T1059)\", \"T1566\", \"\", \"  \"]` yielded `techniqueID` `T1059, T1566` with the blanks dropped."
    },
    {
      "text": "One `core.detections` call builds the Detection & Response report from a matrix and technique IDs, resolving the STIX bundle itself; no caller passes `mitre_data` or `atlas_data`.",
      "state": "observed",
      "note": "MCP `get_detection_report` built reports from matrix + IDs alone: Enterprise (full detection strategies + analytics + log sources), ICS (`Detection of Command-Line Interface (DET0760)`), ATLAS (degrades to the mitigations-only note, as designed). Pages 1 and 2 each offered a working `Download Detection & Response` in the browser for all three matrices. Grep over the repo: no caller outside `core/detections.py` passes `mitre_data=` or `atlas_data=`."
    },
    {
      "text": "`build_defense_report` still accepts injected `mitre_data` / `atlas_data`, and the existing detections and MCP tests keep using it unchanged.",
      "state": "observed",
      "note": "`build_defense_report` keeps both keyword parameters (core/detections.py:188-195). `git diff origin/main...HEAD --numstat -- tests/` is purely additive — 52/0, 37/0, 20/0 — with an empty diff for `tests/test_mcp_server.py` and zero deleted lines in `tests/test_detections.py`; those pre-existing injection tests still pass in the green run below. This criterion's subject is the test suite, so running it is the surface."
    },
    {
      "text": "The private `_resolve_kill_chain`, `_normalise_ids` and `_defense_markdown` helpers are gone from the MCP server.",
      "state": "observed",
      "note": "`grep -rn '_resolve_kill_chain\\|_normalise_ids\\|_defense_markdown' --include=*.py .` returns nothing (exit 1). The server still initialises over stdio and `tools/list` returns all 10 tools; every tool exercised below answered, and the process exited 0."
    },
    {
      "text": "New tests cover the matrix dispatch in both new entry points, including the ATLAS path and an unknown matrix.",
      "state": "observed",
      "note": "Not vacuous — five targeted mutations each turned the matching new test red, then were reverted: (1) routing ATLAS to the threat-group resolver in `resolve_kill_chain` failed `test_dispatches_atlas_to_case_study_resolver` + `test_atlas_ignores_seed`; (2) deleting its `raise ValueError` guard (line 389) failed `test_unknown_matrix_raises` with `DID NOT RAISE`; (3) routing ATLAS down the mitre path in `resolve_defense_report` failed `test_atlas_resolves_its_own_bundle`; (4) returning a report instead of `None` for an unknown matrix failed `test_unknown_matrix_returns_none`; (5) dropping the blank filter in `normalise_technique_ids` failed `test_blank_entries_dropped`. Tree confirmed clean afterwards."
    },
    {
      "text": "`pytest` is green, and generated scenarios, Navigator layers and detection reports are unchanged for the same inputs.",
      "state": "observed",
      "note": "`pytest` under `set -o pipefail`: 350 passed, 7 skipped, exit 0 (the 7 are the `@pytest.mark.browser` tests, no Chromium for pytest here; CI's `Browser checks` job is green on this SHA). Unchanged-output was observed by running each surface on origin/main (98030ab) and on the head and diffing: 14 MCP calls covering kill chains for all three matrices, the built prompt `messages`, detection reports, Navigator layers and four error paths — identical, md5 bdea64f5; 5 MCP generate-tool calls incl. `include_detection` for Enterprise/ATLAS/custom — identical, md5 74b5d3c6; the Custom Scenarios page's prompt, layer and detection report for the same 6-technique selection — identical; and page 1's ATLAS path (no sampling, so deterministic) prompt, layer and detection report — identical. Generation was driven through a local OpenAI-compatible stub, so what was compared is the prompt bytes the app sends, not model output."
    }
  ],
  "open": [
    "core/detections.py:34 — the new module-level `from core import attack_data as ad` pulls mitreattack/stix2/pandas into every importer of core.detections. Measured here: `import core.scenario_page` 8.4s on main -> 12.2s on the head, paid by the AI Insider page (page 3), which needs no MITRE data. A function-local import inside resolve_defense_report keeps the seam and drops the cost. Low severity, outside the acceptance criteria — not fixed.",
    "No LLM provider is reachable from this sandbox (deny-by-default egress), so all four scenario generations ran against a local stub endpoint. Real model output was never generated; prompt bytes were compared instead.",
    "The 7 browser tests skipped under plain `pytest` here (no Chromium installed for pytest). CI's separate `Browser checks` job reports success on this SHA.",
    "Page 2 still branches on the matrix to build its technique *options* list (lines 154-160, 196-202). Out of this ticket's scope — the issue scoped three deepenings and this is none of them — noted only so a later reader is not surprised."
  ]
}

verified at 4fc9c90 — all 8 acceptance criteria carry observed evidence.

The three new seams were driven through their outermost interfaces rather than imported: the Threat Group and Custom Scenarios pages in a headless Chrome against a live streamlit run, and the MCP server over real stdio JSON-RPC. One call now resolves a kill chain for Enterprise, ICS and ATLAS on both surfaces; the Custom Scenarios page and the MCP techniques argument normalise the same 6-technique selection to a byte-identical Navigator layer; and resolve_defense_report builds reports from matrix + IDs alone, degrading to mitigations-only on ATLAS.

"Unchanged for the same inputs" was observed rather than inferred: every surface was run on origin/main and on the head and the outputs diffed — 14 MCP calls, 5 MCP generate calls, and two deterministic page flows — all identical. Failure paths behave: with the endpoint pointed at a dead port the page reports Nothing downstream ran, offers no downloads, and leaves no traceback in the server log; unknown tool, unknown matrix and a missing required argument all come back as isError: true without taking the server down.

The new tests were checked for vacuity: five mutations to the new dispatch code each turned the matching test red.

One thing left open, not a criterion: core/detections.py now imports core.attack_data at module level, adding ~4s to core.scenario_page's cold import (8.4s → 12.2s measured here) for the AI Insider page, which needs no MITRE data. A function-local import would keep the seam without the cost.

@mrwadams mrwadams added verified pr-verify holds observed evidence for every acceptance criterion at the current head and removed awaiting-verification Opened by agent-loop; not yet verified against a running app labels Sep 15, 2026
@mrwadams
mrwadams marked this pull request as ready for review September 15, 2026 05:30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

verified pr-verify holds observed evidence for every acceptance criterion at the current head

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Deepen the MITRE resolution seam: one kill-chain resolver, one technique-ID form, one defence entry point

3 participants