fix: the leet near-miss floor was set three positions too high (#825) - #877
Conversation
|
📄 Docs preview: https://8eb3edc3.disarm-docs.pages.dev |
There was a problem hiding this comment.
🟡 Changes recommended
The new near-miss floor test contains inaccurate decode commentary/test data and one updated assertion is weaker than necessary, reducing the regression value of the test suite.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Pull request overview
This PR fixes a coverage gap in the inspect_anomalies leet detector by lowering the near-miss (1-edit) decode floor from 6 to 5, so short 1-for-l spoofs (which can’t hit the exact-decode path) are detected consistently across brand-length tokens. It also updates tests, documentation, and the changelog to reflect the two leet sub-paths and the rationale for the chosen threshold.
Changes:
- Lower leet near-miss decode minimum length to 5 via a named constant in the Rust core.
- Update and add pytest coverage to pin behavior on both sides of the new boundary (including previously “clean” cases that are now correctly reported).
- Document the near-miss leet path and record the fix and measurement rationale in docs and CHANGELOG.
File summaries
| File | Description |
|---|---|
| src/anomalies.rs | Introduces NEAR_MISS_MIN_LEN = 5 and applies it to the leet near-miss (nearest) path with expanded rationale. |
| tests/test_leet_segmentation.py | Updates #726-related expectations for cases now caught via the near-miss path. |
| tests/test_leet_near_miss_floor.py | Adds new regression tests pinning the near-miss floor boundary and intended false-positive guards. |
| docs/user-guide/anomaly-detection.md | Updates leet documentation to describe both exact and near-miss paths and their floors. |
| CHANGELOG.md | Records the behavior change, rationale, and related test expectation updates. |
Review details
- Files reviewed: 5/5 changed files
- Comments generated: 2
- Review effort level: Lite
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
#877 review, two notes, both correct. The negative cases described `k8s` as decoding to `kos` and `b2b` to `bab`. Those are the words `nearest()` *matched* in the measurement, not the decodes — `leet_sub` maps '8' to 'b' and '2' to 'z', so they are `kbs` and `bzb`. The lexicon was built from the wrong list, which left it unclear which entries were one edit from what. Each row is now an explicit (token, decode, neighbour) triple, so the one-edit relationship is checkable instead of described. The decode is never in the lexicon: seeding it would make these exact-path hits and the test would measure the wrong path. That exposed a second gap the note did not raise. A token that never enters the leet branch — too short a base, a literal number — passes a "does not fire" test for a reason with nothing to do with the floor, and two of the original rows (`x86`, `3pm`) were exactly that. Every row now has a companion assertion that puts the decode itself in the lexicon and requires the exact path to fire, which proves the branch is reachable and the near-miss floor is the only thing declining it. The second note: `Finding.detail` for `leet` is exactly the matched lexicon word, so the `in` check is now `==`. Containment would accept a longer word that merely contains the expected one. Assisted-by: Claude Opus 5 (1M context) <noreply@anthropic.com> Signed-off-by: Richard Quinn <quinn.richard@gmail.com>
bd1e5aa to
6ee7e14
Compare
#877 review, two notes, both correct. The negative cases described `k8s` as decoding to `kos` and `b2b` to `bab`. Those are the words `nearest()` *matched* in the measurement, not the decodes — `leet_sub` maps '8' to 'b' and '2' to 'z', so they are `kbs` and `bzb`. The lexicon was built from the wrong list, which left it unclear which entries were one edit from what. Each row is now an explicit (token, decode, neighbour) triple, so the one-edit relationship is checkable instead of described. The decode is never in the lexicon: seeding it would make these exact-path hits and the test would measure the wrong path. That exposed a second gap the note did not raise. A token that never enters the leet branch — too short a base, a literal number — passes a "does not fire" test for a reason with nothing to do with the floor, and two of the original rows (`x86`, `3pm`) were exactly that. Every row now has a companion assertion that puts the decode itself in the lexicon and requires the exact path to fire, which proves the branch is reachable and the near-miss floor is the only thing declining it. The second note: `Finding.detail` for `leet` is exactly the matched lexicon word, so the `in` check is now `==`. Containment would accept a longer word that merely contains the expected one. Assisted-by: Claude Opus 5 (1M context) <noreply@anthropic.com> Signed-off-by: Richard Quinn <quinn.richard@gmail.com>
The `leet` branch has two sub-paths: the decode is a lexicon word, or the decode is one edit from one. The first has a floor of three. The second carried an uncommented `d.chars().count() >= 6` from #393. `leet_sub` maps `'1'` to `'i'` rather than `'l'`. That is correct — `1` looks like `i` in most faces — but it means a `1`-for-`l` substitution never decodes *exactly*, and can only ever be caught by the second path. Below six characters that path does not run, so the whole substitution class went unreported on short targets. Same brand, same five letters, one digit each: `l0gin` was caught and `1ogin` was not. Measured, 0 of 5 five-letter brands against 7 of 7 six-letter ones. Now five, as a named constant carrying the measurement. Over 65 ordinary digit-bearing tokens (`mp3`, `k8s`, `sha1`, `i18n`, `rtx4090`, …) against a 234k-word lexicon, and 8 single-substitution brand spoofs: floor | false positives / 65 | spoofs caught / 8 ------|----------------------|------------------ 3 | 22 | 8 4 | 12 | 8 5 | 5 | 7 6 | 4 | 3 Five costs exactly one false positive more than six — `top10`, whose decode `topio` is one edit from a word — and more than doubles the spoofs caught. Four costs eight more to gain one (`1yft`, four letters), which is the wrong side of the knee. The issue argued the floor should go entirely, on the grounds that the exact path already fires below it and produces false positives anyway. The measurement does not support that: the exact path contributes 4 of the 65 and removing the floor takes the total to 22. The floor is doing work; it was set too high. That reasoning is on the constant, so the next reader does not repeat the measurement. Two rows of #726's table moved with it. `gn0r3!` and `!dm1n` were asserted **clean**, and the test gave the six-character floor as the reason — freezing the floor's own defect as the correct answer. Both are one edit from the word they imitate, `ignore` and `admin`, and both are now reported. The test is inverted rather than deleted, and says why. The near-miss path was also undocumented. `docs/user-guide/anomaly-detection.md` described the exact path only, which predicts neither `paypa1` firing nor `app1e` staying clean. Refs #762 Closes #825 Assisted-by: Claude Opus 5 (1M context) <noreply@anthropic.com> Signed-off-by: Richard Quinn <quinn.richard@gmail.com>
#877 review, two notes, both correct. The negative cases described `k8s` as decoding to `kos` and `b2b` to `bab`. Those are the words `nearest()` *matched* in the measurement, not the decodes — `leet_sub` maps '8' to 'b' and '2' to 'z', so they are `kbs` and `bzb`. The lexicon was built from the wrong list, which left it unclear which entries were one edit from what. Each row is now an explicit (token, decode, neighbour) triple, so the one-edit relationship is checkable instead of described. The decode is never in the lexicon: seeding it would make these exact-path hits and the test would measure the wrong path. That exposed a second gap the note did not raise. A token that never enters the leet branch — too short a base, a literal number — passes a "does not fire" test for a reason with nothing to do with the floor, and two of the original rows (`x86`, `3pm`) were exactly that. Every row now has a companion assertion that puts the decode itself in the lexicon and requires the exact path to fire, which proves the branch is reachable and the near-miss floor is the only thing declining it. The second note: `Finding.detail` for `leet` is exactly the matched lexicon word, so the `in` check is now `==`. Containment would accept a longer word that merely contains the expected one. Assisted-by: Claude Opus 5 (1M context) <noreply@anthropic.com> Signed-off-by: Richard Quinn <quinn.richard@gmail.com>
6ee7e14 to
2256b41
Compare
Closes #825. Refs #762.
The defect
The
leetbranch has two sub-paths: the decode is a lexicon word, or the decode is one edit from one. The first has a floor of three; the second carried an uncommented>= 6since #393.leet_submaps'1'to'i', not'l'— correct, since1looks likeiin most faces. The consequence is that a1-for-lsubstitution never decodes exactly, so the exact path can never see it and only the near-miss path can. Below six characters, that path does not run.Measured against the issue's table: 0 of 5 five-letter brands, 7 of 7 six-letter ones.
Choosing the new value
The issue argued the floor should go entirely, on the grounds that the exact path fires below it and produces false positives anyway. I measured that and it does not hold. Over 65 ordinary digit-bearing tokens (
mp3,k8s,sha1,i18n,rtx4090,1080p, …) against a 234k-word lexicon, plus 8 single-substitution brand spoofs:The exact path contributes 4 of those 65 on its own, so removing the floor adds 18 new false positives —
k8s→kos,b2b→bab,es6→es. The floor is doing real work. It was set three positions too high, not wrongly present.Five is the knee: one false positive more than six (
top10, decodetopio), and it more than doubles the spoofs caught. Four costs eight more to gain one (1yft, four letters).After: 4 of 5 and 7 of 7.
1yftis the deliberate cost and has its own pinned test.The table lives on the constant's doc comment, so the next person to touch the floor does not have to redo the measurement — which is why it had no comment in the first place.
Two rows of #726's table moved
gn0r3!and!dm1nwere asserted clean, and the test named the six-character floor as the reason:That is a test freezing the floor's own defect as the correct answer. Both are one edit from the word they imitate —
ignoreandadmin— and both are now reported. The test is inverted rather than deleted, and explains what changed.Also
The near-miss path was undocumented.
docs/user-guide/anomaly-detection.mddescribed the exact path only, which predicts neitherpaypa1firing norapp1estaying clean. Both paths and both floors are now on the page.tests/test_leet_near_miss_floor.pypins both sides of the boundary — the four newly-caught spoofs, the seven that must not regress,1yftbelow the floor, and eight short technical tokens that must not start firing. Its negative cases use a lexicon of one-edit neighbours rather than the decodes themselves; seeding the decode would have made them exact-path hits and measured the wrong path.Full gate green:
cargo test723,pytest6,275, doc tests 41 pages, clippy both feature sets,perf_lint.sh,cargo doc,mkdocs --strict. Gate is in the shared core, so every binding inherits it; no binding-specific work.🤖 Generated with Claude Code