fix: regenerate screenshots, and two bugs they exposed - #8
Merged
Conversation
All four README screenshots showed the 11-technique catalog. Regenerated
against the current 24-technique build, driving the real commands and the
real web UI rather than mocking anything:
- cli-list.png Rich export_svg of the actual cmd_list output, rendered
to PNG through headless Chrome. Console width raised
100 -> 110 columns, which removes most of the row
wrapping that 24 entries would otherwise cause.
- webui-emitter.png Emitter tab, REP-001 selected.
- webui-run.png REP-004 mid-run, collector destination on so the run
is rate-limited and the eps waveform is representative.
- webui-terminal.png Terminal tab with the menu listing all 24.
Capturing them surfaced two real bugs, both fixed here with tests.
1. The Rich menu's key legend was hardcoded "[1-11] technique" while the
catalog held 24. The selection validator immediately below it has always
bounded on len(catalog.techniques), so the hint contradicted the code
and told operators that techniques 12-24 could not be selected. The
legend is now derived, extracted as _key_hint() so a test can pin it to
the catalog size.
2. The embedded terminal corrupted box-drawing characters. pty_bridge
decoded every os.read chunk independently with errors="replace", so a
chunk boundary landing mid-UTF-8-sequence destroyed that character
permanently. Box-drawing glyphs are three bytes each, so the bigger menu
table made a straddled boundary likely and the table border rendered
with U+FFFD in it. Now uses a single incremental decoder per connection
that holds an incomplete tail until the rest of the sequence arrives.
Verified end to end, not just in unit tests: the xterm buffer read back
over CDP contained U+FFFD inside the border before the fix and is clean
after it.
Tests: 443 pass, up from 438. The decoder tests assert every possible split
point of a border string round-trips, and pin the old naive behavior so the
regression cannot return quietly.
404SecNotFound
added a commit
that referenced
this pull request
Sep 1, 2026
…(roadmap #8) (#97) * docs: reference detection spec for REP-001 + spec/catalog sync guard (roadmap #8) Blueprint differentiator 5 promises telemetry and detection ship together, but the ndr_rule/ndr_uc labels had no rule content behind them. This authors the first reference detection spec, phased on the pilot technique. - docs/detection-specs/REP-001.md: NDR-C2-001, the periodicity detection its telemetry is shaped to exercise. SIEM-neutral pseudocode (grouped inter-arrival CV + tight byte band), thresholds derived from the catalog presets (jitter -> CV bound), the load-bearing FP tuning (legit software beacons are periodic too; allowlist known-good egress; browsing separates on record count and byte variance per the benign baseline), how to exercise it with --pace plan, and what a green result does and does not prove (transferability transfers; delivery loopback-only until first observed fire). - docs/detection-specs/README.md: the phased index (pilot, then parser-only, then the rest after first fire) and the not-a-generated-rule boundary. - tests/test_detection_specs.py: fails when the catalog moves (rule id, uc, ATT&CK technique, transferability) and a spec does not follow, and when a spec file names a technique that does not exist. Written documentation only; no Sigma/AIE rule is generated at runtime. 1092 tests; gates clean. * review: make the spec sync guard table-authoritative and generic; fix en-dash From /code-review on PR #97, verified before fixing: - The guard proved less than its name claimed (the recurring class). It now parses each spec's own header TABLE and asserts exact matches, generic across every REP-*.md so a new spec is guarded the moment it lands: - ATT&CK is an exact set comparison, so a spec still naming a technique the catalog dropped fails (presence-only could not catch that). - Transferability is read from the header cell, not matched anywhere in prose. - Rule id / use case / technique read from the header cell. - Added a REP-001 preset guard (interval per intensity, dpt_choices) for the README's 'every threshold ties to a catalog preset' promise. Positive control: flipping the transferability cell to contradict the catalog turns it red. - Index sync now checks rule id, use case AND filename per authored spec. - Non-vacuous: pins the pilot spec exists so the glob-driven tests exercise something. - A guard that no spec carries an em- or en-dash (both banned in docs). Fixes the one en-dash the earlier em-dash-only grep missed (150-2000). 1094 tests; black/ruff/mypy clean.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
All four README screenshots showed the 11-technique catalog. Regenerated against the current 24-technique build.
Capturing them surfaced two real bugs, both fixed here with tests.
Bug 1: the menu key legend lied
replicant menuprinted a hardcoded[1-11] techniquewhile the catalog held 24. The selection validator directly below it has always bounded onlen(catalog.techniques), so the hint contradicted the code and told operators that techniques 12-24 could not be selected.Now derived, and extracted as
_key_hint()so a test can pin it to the catalog size.Bug 2: the embedded terminal corrupted box-drawing characters
pty_bridgedecoded everyos.readchunk independently:os.readreturns whatever bytes are ready, so a chunk boundary can land mid-UTF-8-sequence. Witherrors="replace"those split bytes becomeU+FFFDpermanently. Box-drawing glyphs are 3 bytes each, so the bigger menu table made a straddled boundary likely, and the table border rendered corrupted:Fixed with a single incremental decoder per connection that holds an incomplete tail until the rest of the sequence arrives.
Verified end to end, not just in unit tests. Reading the live xterm buffer back over CDP showed
U+FFFDinside the border before the fix and a clean border after it. This is a user-facing rendering bug in the product, not just a screenshot problem: any sufficiently large terminal output could hit it.How the screenshots were produced
Driving the real commands and the real UI, so they cannot drift from behavior:
cli-list.pngexport_svgof the actualcmd_listoutput, rendered to PNG via headless Chrome. Console width 100 to 110 columns, which removes most of the row wrapping 24 entries would otherwise causewebui-emitter.pngwebui-run.pngwebui-terminal.pngOne detail worth noting on
webui-run.png: with both destination toggles off the run is a dry-run that finishes in ~3s at 43k eps, which is unrepresentative and visibly contradicts thecap 2000readout beside it. The capture now enables the collector destination, so the run is rate-limited and the waveform matches what an operator actually sees (1818 eps, mid-run).Verification
443 tests pass, up from 438. The decoder tests assert every possible split point of a border string round-trips, and pin the old naive behavior so the regression cannot return quietly. ruff, black, mypy clean.
Note
cli-list.pngis now 1440x1095 rather than 1440x810: 24 rows is simply taller than 11. The other three keep their original dimensions.