open source · MIT · standard library only

witness

A DFIR agent that can only report what it can prove a tool said.

A finding carries the record it came from, the field, and the value it claims that field holds. Before anything reaches the report, plain Python re-opens the captured tool output and compares. No second language model is asked, because a model can be argued out of a correct refutation and a comparison cannot.

$python demo.pyclick to copy the whole thing $pip install witness-cliclick to copy Break a finding ↓ Watch the tour ▶ View source

The clone runs the proof — fixtures, the adversarial catalogue, the whole case in three seconds. The install points the gate at your evidence. The package is witness-cli because witness on PyPI was taken in 2017; the module and the command are still witness.

scroll
0
language models in the gate
The oracle is a field comparison in plain Python. Nothing in the admission path can be persuaded, and nothing in it needs a key.
0
crafted attacks
Nine families, each stating one invariant and then attacking it — alongside 1,083 scored claims in 29 classes of the way analysis actually goes wrong.
0
seconds, start to finish
Clone it and run one command. No install, no API key, no network, no 3 GB memory image to download first.
the failure mode that survives review

The tool ran. The citation is real. One field is wrong.

Nobody worries about the agent that invents a tool it never ran — that one is caught the moment somebody looks. The dangerous finding is the one where the tool genuinely ran, the parent process really is PowerShell, the story is exactly right, and two digits changed places on the way to the sentence.

psscan.txt · what the tool actually printed
PID PPID ImageFileName 3720 1332 rundll32.exe
the finding that got written
rundll32.exe (3720) was spawned by powershell.exe (1233).
Everything about this is right except the number, and the number is the part a responder acts on.

Hand that to a second language model and ask it to check. It reads a fluent, internally consistent account of a real intrusion and agrees, because the claim is overwhelmingly plausible — and plausibility is the only thing it can measure. The transposition is not a reasoning failure. It is a transcription failure, and it needs a reader, not a critic.

the centerpiece · interactive

Change one digit. Watch it get thrown out.

Both gates below run on real fixtures from the SANS 2018 compromised image. Nothing here is a mock-up of the logic — it is the logic, which is small enough to fit on a page precisely because it is not a model.

the finding · edit the claimed value
rundll32.exe (3720) was spawned by powershell.exe (1233).
1233
binding: process(base-wkstn-05, pid=3720).ppid == 1233
fixtures/wkstn05/psscan.txt · what was captured
PID PPID ImageFileName Threads 1332 3920 powershell.exe 10 2676 776 WmiPrvSE.exe 10 3920 2676 powershell.exe 12 4412 4844 chrome.exe 14 960 5472 iexplore.exe 19 3548 652 subject_srv.ex 13 5692 652 taskhost.exe 0 3440 6908 csrss.exe 10 4848 132 MSOSYNC.EXE 24 2780 4844 chrome.exe 13 3720 1332 rundll32.exe 0
Trimmed to the columns in play. The highlighted row is the one the binding names — and the only row it is allowed to read.
REFUSED
the finding · every binding verifies
rundll32.exe (3720) on base-wkstn-05 contains injected code.
binding: process(base-wkstn-05, pid=3720).pid == 3720 — which is true, so gate 1 passes and the claim keeps going.
so gate 2 asks the only question left
What tool output would prove this wrong — and did anyone run it?
the falsification matrix
state admits? what it means corroborated yes the refuting tool ran and agrees refuted no the refuting tool ran and disagrees unavailable no nobody looked
Three states, one of which is the whole point. Most tools treat a missing check as a pass, because silence is easy to mistake for consent. Here the absence of evidence is recorded as the absence of evidence, and the finding is held with the missing tool named.
src/witness/falsify.py · the whole check
# what would refute a process_injection claim EvidenceCheck( name="malfind_shows_injected_memory", tool="windows.malfind", kind="injection_hit", match={"pid": "pid"}, expect="at_least_one", )
A finding type declares, up front, the tool that could kill it. There is no model in this file — the hard part was deciding what refutes what, and that decision is written down where it can be argued with.
REFUSED

Neither gate has an opinion, a temperature, or a bad day. Gate 1 is a comparison against a record the finding itself named. Gate 2 is a lookup for a tool that either ran or did not. That is the entire oracle — and it is why the same case, run twice, produces the same report to the byte.

the disagreement this project is built on

A second model is the wrong kind of oracle.

Checking a language model with a language model is the reflex, and it does catch things. But it inherits the property that made the first model unreliable: it is a persuadable reader of prose, being asked to referee a claim about a number.

model as judge

Scores plausibility, and calls it truth.

A transposed PID inside an otherwise flawless account of a real intrusion reads as correct, because it is correct in every respect the judge can assess.

It can be talked round. A confident restatement of the claim moves the verdict, which is exactly the failure being defended against, one layer up.

It is non-deterministic, so the same report checked twice can come back differently, and neither answer can be audited later.

field comparison

Answers a smaller question, exactly.

It reads one field of one named record and compares. It has no view on whether the story is compelling, which is why the story cannot sway it.

It cannot be argued with, because there is no argument to make — the record either holds that value or it does not.

It is deterministic and auditable. The refusal names the actual defect, so the ledger says why a finding died, not merely that it did.

The trade is deliberate: a comparison answers a much smaller question than a judge does, and it answers it perfectly. Everything the smaller question cannot see is handled by moving work into it — by decoding evidence rather than describing it, so there is something concrete left to compare against.

the cost, published next to the score

The result worth showing is one it gets wrong.

An accuracy report with no cost column is an advertisement. A tool that refuses everything has perfect precision, so precision on its own is free — the number that has to sit beside it is how much true work was thrown away getting there.

100%
precision — of 519 admitted findings, 519 were supported
100%
recall — of 519 supported claims, 519 were admitted
100%
reason — of 564 refusals, 564 named the actual defect

The third line is the one that separates accuracy from luck, and it is the one that found a defect. Fifty-seven claims were being refused correctly and logged for the wrong reason. 1,083 is classes times records, not 1,083 independent experiments — the per-class breakdown in the repo is the honest way to read it.

the claim
The host beaconed to external C2 www.venetodns.trade.
verdict · refused
This is the intrusion's real command and control, and witness throws it out.
The domain is a string in the image, tied to nothing that ran. Being right about the case is not the same as being shown by the evidence — and the moment that distinction is allowed to blur, the gate is decorative.

It is not a bug that got left in. It is the property, doing its job on a case where the property is expensive, and it is the first thing worth putting in front of a reviewer. The same run admits the C2 that is shown by the evidence — an address carried two layers down inside a carved PowerShell command line, reachable only by decoding the stager rather than describing it.

no clocks, no counters, no drift

Run it twice. Get the same bytes.

Every identity in the system is derived from content. Records are hash-chained, and the head of the chain is a fingerprint of the case rather than of the moment it was produced — so there is no timestamp, sequence number or random seed anywhere for two runs to disagree about.

hash-chained records content-derived identity byte-identical replay every refusal logged with its reason read-only against evidence

Which makes a report checkable by someone who does not trust it. Re-run the case, compare the SHA-256, and any divergence is a real difference in the evidence or the code — never noise. A ledger that shifts between runs cannot be used to contradict anybody, and a forensic artefact that cannot be used to contradict anybody is decoration.

where it came from

A rebuild of an entry that didn't win.

In June I submitted find-evil to the SANS FIND EVIL! hackathon — an IOC-lifecycle layer that catches a forensic agent's hallucinated indicators. It didn't place. Reading the entries that did, the ones I looked at had reached for the same defence I had: put a second language model in front of the first and have it argue. That is the part I now think is wrong, and this is the rebuild from scratch — same problem, an oracle that cannot be talked round.

Roughly 5,000 lines, standard library only, enforced by a test that parses every shipped module and fails on any import that is not local or stdlib. 349 tests, including one that copies the repo to a temp directory and runs the demo with an isolated interpreter — because a clone-and-run claim in a README is worth exactly as much as the test that keeps it true.

bind the field · ask what would refute it · log the reason it died

python demo.py

$git clone … && python demo.pyclick to copy $pip install witness-clior install it and use your own evidence
copied ✓