Conversation
…que-ID form, one defence entry point Implements #111 (agent-loop).
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
{
"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 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 "Unchanged for the same inputs" was observed rather than inferred: every surface was run on 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: |
Automated by agent-loop — the agent worked this issue on a fresh clone of
mrwadams/attackgeninside a disposable sandbox; changes were gated outside the agent.Closes #111
How to test
Then open the printed local URL.
Confirm each acceptance criterion:
core.attack_datacall resolves a kill chain for Enterprise, ICS and ATLAS; no caller outsidecorebranches on the matrix to choose a resolver.KillChainexposes its technique IDs; no caller re-derives them from its technique records."Name (ID)"labels or bare IDs into bare IDs, and the Custom Scenarios page and the MCPtechniquesargument both use it, so the same selection normalises identically.core.detectionscall builds the Detection & Response report from a matrix and technique IDs, resolving the STIX bundle itself; no caller passesmitre_dataoratlas_data.build_defense_reportstill accepts injectedmitre_data/atlas_data, and the existing detections and MCP tests keep using it unchanged._resolve_kill_chain,_normalise_idsand_defense_markdownhelpers are gone from the MCP server.pytestis green, and generated scenarios, Navigator layers and detection reports are unchanged for the same inputs.Gates (non-authoritative)