Skip to content

fix: regenerate screenshots, and two bugs they exposed - #8

Merged
404SecNotFound merged 1 commit into
mainfrom
fix/screenshots-and-terminal-bugs
Jul 28, 2026
Merged

fix: regenerate screenshots, and two bugs they exposed#8
404SecNotFound merged 1 commit into
mainfrom
fix/screenshots-and-terminal-bugs

Conversation

@404SecNotFound

Copy link
Copy Markdown
Owner

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 menu printed a hardcoded [1-11] technique while the catalog held 24. The selection validator directly 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.

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_bridge decoded every os.read chunk independently:

await websocket.send_text(data.decode("utf-8", "replace"))

os.read returns whatever bytes are ready, so a chunk boundary can land mid-UTF-8-sequence. With errors="replace" those split bytes become U+FFFD permanently. 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+FFFD inside 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:

Image How
cli-list.png Rich export_svg of the actual cmd_list output, rendered to PNG via headless Chrome. Console width 100 to 110 columns, which removes most of the row wrapping 24 entries would otherwise cause
webui-emitter.png Emitter tab, REP-001 selected
webui-run.png REP-004 mid-run
webui-terminal.png Terminal tab, menu listing all 24

One 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 the cap 2000 readout 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.png is now 1440x1095 rather than 1440x810: 24 rows is simply taller than 11. The other three keep their original dimensions.

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
404SecNotFound merged commit 3cb4073 into main Jul 28, 2026
8 checks passed
@404SecNotFound
404SecNotFound deleted the fix/screenshots-and-terminal-bugs branch July 28, 2026 08:15
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.
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