Repository navigation
Watch the end of the kill chain, and stop the triage layer matching text we never send - #45
Merged
Merged
Conversation
…ext we never send Add six collection/exfiltration/impact rules, add a Windows rule-coverage invariant that found eleven unexercised rules, grow the corpus 19 to 35, fix the criteria markers that keyed on Event Viewer's rendering instead of the agent's inserts, and split the coverage assertion into a gap and a decision. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
d3vhex
added a commit
that referenced
this pull request
Sep 8, 2026
…hain Watch the end of the kill chain, and stop the triage layer matching text we never send
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.
Why
Measuring ATT&CK coverage by tactic instead of counting rules gave an
uncomfortable answer:
Every rule we shipped watched an intruder arriving, gaining privilege and
staying. Nothing watched them leaving with anything. A host could be swept,
archived and uploaded and the console would stay quiet — while the coverage
page reported 37 rules and 38 techniques, which reads as thorough.
Six rules for the end of the chain
collection 3, exfiltration 2, impact 2; 43 rules, 47 techniques.Each rule keys on a pairing rather than a tool, because the tools are
ordinary.
rar ais a backup job;rar a -hpinto%TEMP%is not.Invoke-RestMethodis how our own automation talks to our own APIs;-Method Post -InFileto transfer.sh is not.One branch was written and then deliberately deleted:
scp/rsyncin theLinux exfiltration rule fired on
rsync -az --delete /srv/data/ backup-01:/srv/data/, which is a scheduled backup. A rule that pages onevery backup gets muted, and a muted rule still counts as coverage — worse
than not shipping it.
The invariant that found eleven older gaps
There was a test asserting every Linux rule is exercised by a corpus case.
There was no Windows equivalent. Adding one found 13 unexercised Windows
rules, 11 of them pre-existing — rules that had never fired against
anything, and nothing could say so.
CORPUS_FIELDShad noImagemapping, so a rule keyed on the child processimage (
win_office_spawns_shell) could not be expressed by any corpus case.It was unexercisable by construction.
The corpus grew 19 → 35 cases, plus six evidence samples. Writing them found
real detection bugs in rules both new and old:
net.exe stopdid not match a rule looking fornet stopcatmatched insidencat, so a netcat reverse shell fired theexfiltration rule
tarmatched insideStart-Sleep, so a clipboard loop fired the archiverule
' -F 'collided withcertutil -funder case-insensitive matchingThe AI triage layer was matching text the agent does not produce
ai/criteria.pyverifies the criterion the model claims, so a supportedcriterion forces CRITICAL. Three of its markers could never have fired:
Those are message templates. Windows stores the fields and renders the
sentence when something asks; the collector joins
StringInsertswith " | "and never asks — 1102 arrives as four fields naming the account, 7045 as
service name, image path, type, start type, account. No prose in either. The
markers matched hand-written corpus lines and nothing else, and both events
have their own Sigma rule keyed on the event id, so one layer covered it while
the other reported coverage it did not have.
Added the
eid=forms alongside the phrases, plus markers for the gaps thenew corpus cases exposed:
ntdsutil ifm(the supported way to extract the ADdatabase never names
ntds.dit),wevtutil cl,amsiInitFailed,advfirewall state off,certutil -urlcache.Removed one.
currentversion\run+\appdata\was still there — the samealternative the comment above it says was removed, written longer. It matched
constructed:t1547-run-key, which the corpus grades SUSPICIOUS, and aforced CRITICAL made that case unscoreable: no answer from the model could
win. Two things hid it — the test used a sample with
CurrentVersionelidedfrom the key path, so the first marker was absent and the assertion passed for
the wrong reason, and the benign-corpus test only looks at
NOT_CRITICALrows.
The coverage test now distinguishes a gap from a decision
test_the_markers_cover_the_constructed_attacksasserted every corpuspositive matches a criterion. That made sense when the corpus was eight cases
written beside the markers. It is now also what proves every Sigma rule fires,
so it grows with every rule — and the assertion had come to read "every new
rule needs a marker beside it".
That is the wrong direction. A marker forces CRITICAL and skips the confidence
gate.
net user /addis a real technique and also what the helpdesk does allmorning;
net group "Domain Admins"is the first thing an administrator typesand the first thing an intruder types. Those belong to the rules, which can
score and be tuned.
Replaced with three tests and a written-down list of the eight cases the
criteria deliberately do not cover, each with its reason:
match — a marker widening until it covers a case somebody argued out is how
appdatagot back inis checked rather than assumed
Plus
test_no_case_below_critical_is_forced_to_critical, which covers theSUSPICIOUS band both existing benign tests skipped.
Testing
2518 passed, 5 skipped. The five are PTY tests on a POSIX host and onecorrelation shape Sigma cannot express.