A Read Is Not an Alibi
I wrote out the verdicts my triage layer is allowed to reach, and the word I had built the whole thing for was not among them.
There is telemetry-only. There is blind. There is corroborated, and there is benign-explained. There is no confirmed, and no path to one.
The tool is a defensive thing that runs on my own machines each morning. Twenty-five small pillars each watch exactly one surface: the bytes of a binary that is supposed to be immutable, the set of loaded kernel modules, what runs at login. Anything that moved since yesterday becomes a finding, and until recently the findings landed in a briefing as an undifferentiated pile, every one equally loud, which is a good way to teach yourself to stop reading a briefing.
So I built the layer that picks up one finding and works out what it means. State a hypothesis, run the query that tests it, write down the answer, negative results included. I assumed the hard part would be deciding when something is real. Nothing on this machine can decide that.
Telemetry agreeing with telemetry
A pillar noticing that a watched executable’s bytes changed has observed one thing: the bytes changed. An upgrade, a rebuild, a compromise all produce the identical delta, and nothing the machine can ask itself resolves that, because every question it can ask is asked with the instruments that raised the alarm.
So the ladder climbs on corroboration instead: another pillar, same run, naming the same subject. That was the design I was proud of, and it is the weaker half of what shipped.
The precedence I ended up writing puts benign-explained above corroborated, and the reason is in a comment I wrote while arguing with myself: the pillars that corroborate a package upgrade are the same ones that would report it. Two of my sensors agreeing is not two pieces of evidence. It is one piece counted twice, because I built them to watch overlapping surfaces on purpose. The verdict text says so when it fires: Still not a confirmation: telemetry agreeing with telemetry is not proof.
Which leaves the top of the ladder occupied by an exoneration. The strongest sentence this thing can produce is that nothing happened.
I did not set out to build that, and an alarm system whose maximum confidence is a dismissal sounds broken. But run the costs. A finding wrongly escalated costs me ten minutes of reading. A finding wrongly dismissed costs me the finding permanently, because a dismissed thing goes into a baseline and the baseline is what tomorrow gets compared against. It never fires again. The alarm side is allowed to be weak, because a weak alarm still ends with a human looking. All of the rigor belongs on the quiet side, where it looks like nothing is happening.
What counts as an excuse
The decisive question, then, is what the tool accepts as a benign cause. Autonomous agents do most of the building here, constantly, and they write to exactly the paths these pillars watch. “An agent did it” explains almost every finding I will ever see, so it had better be checkable rather than comfortable, and it is: the guard those agents run under keeps an append-only record of every tool call they make.
My first version searched that record for the path named in the finding and, on a hit, called the finding explained. That version was wrong, and wrong in the direction that kills you.
An agent that merely read the path did not change it. If a read counts as an alibi, an attacker touching a watched file is exonerated by any unrelated agent that happened to open it that afternoon, and the pivot has explained away the exact case it exists to catch. So the filter is narrower: only a tool call that could have mutated the subject is admitted. Everything else comes back as recent calls name this subject, but none of them could have changed it, which is not an explanation and is not treated as one.
An alibi has to be narrower than the crime. Mine was wider, and a wider alibi covers the guilty.
There is a smaller version of the trap one layer down. When a pivot cannot run at all, because the record is missing or a snapshot was never taken, the honest outcome is not “nothing found.” Nothing found means I looked. A pivot that could not run returns blind, and blind outranks the weakest rung, so an investigation that half failed can never present as a quiet single-source curiosity.
Pardons with no expiry
Before any of this, the tool had exactly one gesture for a finding I judged harmless: an --accept flag that swallowed it into the pillar’s baseline. No reason recorded. No evidence recorded. No expiry.
That is a pardon written in disappearing ink, and I suspect it is the single most widely deployed security control there is. The skipped test. The linter rule switched off in a config nobody has opened since. The advisory marked won’t fix by someone who has left. Each one is a permanent, unreviewed judgement made in about five seconds by a person who wanted the noise to stop, and none of them is ever revisited, because the whole point was to stop it appearing.
The replacement keeps the acceptance and ties it to what it was based on. Verdicting a finding benign now requires a reason in writing, stores a digest of the finding’s rendered label (the line carrying its actual specifics, the byte counts and the tallies), and takes an optional window. Every later run re-evaluates all three. If the digest still matches and the window is open the finding stays accepted, sitting in the briefing under its own heading with the reason attached, never deleted. If the window has passed it is expired. If the finding’s shape has changed it is returned regardless of the window, because deciding that a source of forty failed authentications was harmless is not a decision about four thousand.
The precedence between those last two took a minute to get right, and I left the reasoning in the code: a changed shape outranks a passed deadline, because “the risk itself changed” is more specific and more actionable than “your clock ran out.”
I verdicted this host’s one open finding benign, with a reason and a window, and watched the next morning’s run drop it out of the drift block while two genuinely new findings surfaced in its place. That is the behaviour I wanted: not silence, but a smaller, current list.
The old flag made findings go away. The ledger makes them come back when the story stops matching. Every excuse in it is now written down in a form a stranger could check, including the ones I gave myself.