Skip to content

feat: group, filter, and document the catalog in the web UI (spec Part 2) - #11

Merged
404SecNotFound merged 2 commits into
mainfrom
feat/webui-nav
Jul 29, 2026
Merged

feat: group, filter, and document the catalog in the web UI (spec Part 2)#11
404SecNotFound merged 2 commits into
mainfrom
feat/webui-nav

Conversation

@404SecNotFound

Copy link
Copy Markdown
Owner

Part 2 of tasks/webui-access-and-nav-spec.md. Stacked on #10 — review that first; this PR's base is feat/webui-access, so the diff shown here is Part 2 only.

The problem

The left rail was a flat list of 24 techniques with no filter, no grouping, and no way to reach the reference material without leaving the UI. The run form had no anchor control at all.

What changed

Grouped by ATT&CK tactic, collapsible, count per group, a multi-tactic technique under each of its tactics. The ordering lives in webui/src/lib/catalogView.ts rather than the component so the rule is unit-testable, and the kill-chain order is written out explicitly instead of derived — it is neither alphabetical nor numeric. Reconnaissance is the first tactic and carries the highest number (TA0043), and Exfiltration (TA0010) follows Command and Control (TA0011). Both obvious sort keys get it wrong, so there is a test that fails under either.

A filter box over technique id, name, use case id, and ATT&CK technique id simultaneously, because an engineer arriving from a detection backlog has whichever identifier their ticket happened to carry. Groups that empty out disappear; the header switches to N of 24.

Log-type toggles for the five render paths the catalog uses.

A Docs tabGET /api/docs, GET /api/docs/{id} over a fixed allowlist (three vendor CEF references, two catalog research notes). The id is a dictionary key and is never joined onto a path, so a traversal attempt resolves to nothing rather than to a file; there is a parametrised test pinning that. Rendered with marked (MIT), lazy-loaded into its own chunk the way xterm already is, so the Emitter view's first paint is unchanged (main bundle +0.4 kB, docs chunk 44 kB / 13.8 kB gzip).

An anchor control in the run form, now or fixed, defaulting to now for a live send and fixed for file output.

The anchor is the substantive fix

The web path had no way to set the anchor. Every live send from the UI therefore carried the deterministic default: the CEF eventtime sits at the anchor while the syslog header is stamped at send time. On a SIEM that keys on receipt time nothing looks wrong. On one that keys on parsed event time, no recent-window rule fires — and the operator cannot tell that from a broken detection. That ambiguity is the thing this project exists to remove.

POST /api/runs now accepts anchor (now, fixed, an epoch, or ISO-8601, through the same parse_anchor the CLI uses) and returns the resolved anchor_epoch plus an anchor_warning. The form's pre-flight notice states the consequence rather than re-implementing the server's staleness threshold, so there is no duplicated rule to drift out of sync.

docs/ is not in the wheel

pyproject.toml packages replicant* only and there is no MANIFEST.in, so docs/ — like webui/dist and data/ — ships with the repository, not the package. The Docs tab reports that plainly on a non-editable install instead of failing. This is the pre-existing packaging gap filed at docs/end-to-end-debug-audit-2026-07-21.md:194-215; this PR inherits it deliberately rather than pretending otherwise.

Fixes a UAT case that could not pass

CHAIN-16 asserted the web UI has no scenario surface by grepping replicant/web/server.py for the literal string scenario, expecting zero hits. That file imports replicant.scenario.engine for implemented_technique_ids, so the grep already returned a hit with no scenario feature present. It was never a correct check. It now asserts what it meant — five candidate routes 404, no scenario control in the UI — and App.test.tsx pins the UI half automatically.

Not built: the vendor half of spec item 9

Item 9 asks for toggles for "vendor applicability and log type". The log-type half is here. The vendor half is not, because it would filter nothing: all three vendor profiles implement all six render paths, the catalog uses five of them, so all 24 techniques apply to all 3 vendors and the control could never exclude an entry.

Shipping a toggle that looks like it does something and cannot is the same call as REP-016, which was left unbuildable rather than shipped dishonestly. Options are recorded in tasks/todo.md for a decision. For what it is worth, action is the only other axis that actually splits the catalog (6 values); benign_baseline and implemented are uniform across all 24 and would be equally inert.

Verification

516 Python tests (496 after #10), 64 frontend (23 after #10). black, ruff, mypy clean.

Driven in a real browser against a live server, not only in jsdom: the rail renders grouped with counts, filtering on T1048 narrows to 3 of 24 and correctly lists REP-004 under both Command and Control and Exfiltration, the Docs tab renders the FortiGate reference with working headings, code blocks and tables, the anchor control shows fixed for file output, and the address bar reads http://127.0.0.1:9787 with the token stripped after load.

Part 2 of tasks/webui-access-and-nav-spec.md, stacked on feat/webui-access. The
left rail was a flat list of 24 techniques.

- grouped by ATT&CK tactic, collapsible, count per group, a multi-tactic technique
  under each of its tactics. Ordering logic lives in webui/src/lib/catalogView.ts
  so it is unit-testable, and the kill-chain order is stated explicitly rather than
  derived: it is neither alphabetical nor numeric, because Reconnaissance is the
  first tactic and carries the highest number (TA0043), and Exfiltration (TA0010)
  follows Command and Control (TA0011)
- a filter box matching technique id, name, use case id and ATT&CK id at once, so
  whichever identifier the operator's ticket carried works. Empty groups disappear
  and the header switches to "N of 24"
- log-type toggles for the five render paths the catalog uses
- a Docs tab: GET /api/docs and /api/docs/{id} over a fixed allowlist. The id is a
  dictionary key and is never joined onto a path, so traversal resolves to nothing.
  Rendered with marked (MIT), lazy-loaded into its own chunk the way xterm already
  is, so the Emitter view's first paint is unchanged (main bundle +0.4 kB)
- an anchor control in the run form. POST /api/runs now accepts `anchor` and returns
  the resolved anchor_epoch and an anchor_warning

The anchor is the substantive fix here. The web path had no way to set it, so every
live send from the UI carried the deterministic default: CEF eventtime sits at the
anchor while the syslog header is stamped at send time, and on a SIEM that keys on
parsed event time no recent-window rule fires. That is indistinguishable from a
broken detection, which is the ambiguity this project exists to remove. The form's
pre-flight notice states the consequence rather than re-implementing the server's
staleness threshold, so there is no duplicated rule to drift.

docs/ ships with the repository, not the wheel (pyproject packages `replicant*`
only, no MANIFEST.in), exactly like webui/dist. A non-editable install now gets an
explanation from the Docs tab instead of a failure.

Also fixes UAT case CHAIN-16, which could not pass: it asserted the web UI has no
scenario surface by grepping server.py for the literal string "scenario", but that
file imports replicant.scenario.engine, so the grep already returned a hit with no
scenario feature present. It was never a correct check. Now asserts the five
candidate routes 404 and that no scenario control exists in the UI, and App.test.tsx
pins the UI half automatically.

NOT built: the vendor half of spec item 9. All three vendor profiles implement all
six render paths, so all 24 techniques apply to all 3 vendors and the control could
never exclude an entry. Options recorded in tasks/todo.md for DJR to decide.

516 python tests (496 before), 64 frontend (23 before), black/ruff/mypy clean.
Driven in a real browser as well as jsdom.
- check the premise of a spec item before building it. The vendor-applicability
  filter would have split the catalog zero ways, because all 24 techniques apply
  to all 3 vendors. A control whose output cannot change is decoration, and it is
  the same failure as a test that cannot fail, moved into the UI.
- pick-a-value decisions get measured, not reasoned about. 8787 was chosen by
  reasoning about likely collisions and turned out to be held on the author's own
  machine by an unrelated tool. One lsof would have found it.
@404SecNotFound
404SecNotFound changed the base branch from feat/webui-access to main July 29, 2026 05:29
@404SecNotFound
404SecNotFound merged commit dfe4fcd into main Jul 29, 2026
8 checks passed
@404SecNotFound
404SecNotFound deleted the feat/webui-nav branch July 29, 2026 05:29
404SecNotFound added a commit that referenced this pull request Jul 29, 2026
Both CLAUDE.md and AGENTS.md said 'Next up, not started: group the catalog by
MITRE tactic in the web UI left rail, then a Docs tab'. Both shipped in PR #11.
The file every session reads first was telling the next one to build what already
exists.

This is the third instance of the same failure mode in one day, after the README
describing a UI that had moved on and the roadmap section in that same README.
The pattern is that a feature PR lands and the orienting documents keep describing
the world before it.

Replaces it with a v0.3.0 entry, the three conventions the work established, and
the actual next items (light theme, responsive layout, live-vendor pass). Also
points at the settled vendor licensing position and carries the standing
constraint about never claiming CEF certification.

AGENTS.md is gitignored and was updated in the working tree to match.
404SecNotFound added a commit that referenced this pull request Sep 1, 2026
Execution of the 2026-09 five-persona roadmap: 13 buildable survivors across ten
PRs (#89-#98). Two changes alter emitted output (marker default-on for
non-loopback sends; per-flow packet counts), hence the minor bump. The three
lab-gated items (#4 pilot, #10/#11 refining the unbuilt F2/F5) stay behind the
launch gate; every timing and delivery claim remains loopback-only until the
first observed rule fire.

- version 0.9.0 -> 0.10.0 (pyproject + __init__)
- CHANGELOG [0.10.0] entry (Added/Changed/Fixed)
- README: roadmap-execution bullet + Status badge -> v0.10.0
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant