The parser is a trust boundary. No one audits it.

I'm Johannes. I have spent eighteen years on the response side of security, and I build the machinery that turns a breach dump into an answer you can defend.

The first program that reads a stolen dump is a parser, and almost nobody has looked at it.

Go and find the open-source tool that turns a raw infostealer log into structured records. You get three small repositories. One was last touched in 2024. One of the three counts the files it could not parse. Not one of them counts the lines it dropped inside the files it could. And whatever number you quote to a regulator two weeks later, the count of credentials, the count of people, was decided by that unaudited code at the very start of the pipeline, before anyone in the room asked a question.

The parser is a trust boundary. It ships with no attestation.

That is the whole post, and if it sounds like a small point, sit with what it decides.

The count you defend was set at step zero

In two earlier pieces I argued that the number on a leak site is a claim, not a receipt, and that the truth is in the data, not the headline. Reading the data is how you take the story back.

But reading the data means parsing it first, and the parse is the part everyone waves past. A stealer log is not a clean table. It is one folder per infected machine, a passwords.txt here, a system-info file there, and no two builder versions laying it out the same way, because the tools are cracked and modded and everyone ships their own dialect. Feed that to a parser and the dangerous failure is not a crash. A crash you notice. The dangerous failure is the line that does not match the grammar and quietly disappears, or the family that gets mis-identified so the fields map to the wrong columns.

No error. No count. No trace.

So your parser reads nine hundred thousand lines, emits eight hundred and twenty thousand records, and says nothing about the eighty thousand it dropped. You now have a confident, specific, wrong number, and it looks exactly like a right one. You have not corrected the attacker's lie. You have replaced it with your own, in cleaner formatting.

A silent drop is a lie you will defend later.

An address is not a person, and the parser hides both problems

Here is where it gets worse, and where the interesting work is.

Even a perfect parse gives you rows, not people. The same human appears as a directory entry, a signature block, and a mangled string in a log. Count the rows and you have counted inboxes. The question a regulator actually asks, how many people, needs those to collapse into one.

On a public research corpus, clean and adversary-free, my pipeline extracts roughly 249,000 raw entity mentions from 49,547 emails. They resolve to 710 actual people. A 350 to 1 reduction, on cooperative data. A hostile dump is larger, dirtier, and salted by someone who profits from your overcount.

That 350 to 1 is the gap between counting mentions and counting people. And it sits directly on top of the parse. If the parser silently dropped a fifth of the mentions, the resolution runs on a fifth-blind corpus and produces a confident 568 instead of 710, with no seam to show where the loss happened. Two silent steps, stacked, and the output still looks authoritative.

The thing I keep returning to is that a control is only worth what you can prove about it afterwards. A count is a control. Almost no one can prove theirs.

What an honesty layer actually is

None of this is hard. It is unglamorous, which is different.

An honest parser does three things the three repositories do not.

It counts what it could not read. Lines in, records out, and the difference is a number you publish, not a value you discard. Reject to a quarantine you can open, never continue past a line into the void.

The one repo that does account shows how thin the accounting gets. combolist-check keeps an ingest report with a count of the files it could not parse, and its README says outright that lines which do not match an address pattern are silently skipped. Both are more honesty than the other two manage. But its line counter only ever increments on a pair it accepted. Lines out are recorded, lines in never are, so the one number you would need is the number the run cannot produce. Counting at the wrong granularity is not a smaller version of counting. It is the same silence, one level up.

K, the count of things you could not parse, is the single most useful number in the pipeline and the one nobody reports.

It fingerprints itself onto every record. Which grammar, which version, produced this row. So a re-parse is comparable, and "we re-ran it and the number moved" becomes a diff you can read rather than a mystery.

It binds every record back to the source bytes. Not a copy of the value, a pointer: this mention came from that file, at that offset, through that extractor, on a chain that breaks if anyone edits it underneath. Troy Hunt has written about how he verifies a breach by hand, reading the data against what the service actually holds. This is that instinct, made mechanical: the verification is not a story you tell about the data, it is a property carried by each record.

Put those three together and the count stops being an assertion. Every person in the total is a resolved identity, every resolved identity is a set of mentions, and every mention is provable back to the bytes it came from. You can hand the whole chain to the person who will one day dispute it.

What I got wrong, and what I still cannot do

I did not start with any of this. My own early pipeline overwrote its extracted text in place, at a fixed location, every time it re-ran. That silently destroyed the provenance of which extractor produced which bytes, and worse, it re-introduced credentials a redaction pass had already stripped, because the redaction only ran on a file's first extraction. I had built the exact silent-overwrite failure I am warning you about, and I found it the way you always find these things, by accident, months in.

So I am not writing this from a clean position. I am writing it from the scar.

And my parser still has a K. There is always a residue it cannot read, a builder variant it has never seen. The honest move is not to drive K to zero, which is impossible against an ecosystem of cracked tools that changes every month. The honest move is to count K and publish it. A parser that claims zero unreadable lines is not better than mine. It is lying about the same residue, or it has not looked.

The number I quoted, 350 to 1, is clean cooperative data. I have run hostile corpora through the pipeline as test loads, without an incident clock, and they were dirtier in every dimension. I have not run this end to end inside a live incident, with counsel arguing about whether the data may even be loaded while the clock runs. That difference is not cosmetic, and I will not pretend the test numbers survive it unchanged.

What is still unsolved

The deepest problem here is the same one that shadows everything in breach work: proving absence.

I can show you what the parser read. I can count what it dropped and show you the quarantine. What I cannot easily prove is that the quarantine is complete, that there is not a third silent failure mode I have not instrumented, a line that failed in a way my accounting does not even have a category for. "Here is everything I could not parse" is a sentence I can defend. "That is everything I could not parse" is not.

The parser bounds the floor of what you missed. It does not bound the ceiling. I have not seen anyone articulate that boundary well, and I include my own work in that.

What to take from this

  • The first program that reads a dump is a parser, and it is the least-audited code in the chain.
  • The parser is a trust boundary. It ships with no attestation.
  • A silent drop is a lie you will defend later. A crash you notice; a dropped record you inherit.
  • Count K, the records you could not read. It is the most useful number in the pipeline and the one nobody reports.
  • Fingerprint the parser onto every record, or a re-parse proves nothing.
  • Bind every record to its source bytes, so verification is a property, not a story.
  • An address is not a person. 350 to 1 on clean data, worse on hostile, and the collapse sits on top of the parse.
  • Proving absence is unsolved. You can show what you could not read, never that it was all of it.

If you have built a parser that counts what it drops, or an honesty layer for stealer-log tooling that actually holds up, I want to hear how it works, because everyone I ask describes the same silent continue and the same regret.

Turning a dump into an answer you can defend, with every record traced back to the exact bytes it came from, is the problem I work on during the day: that work is here.


Sources

  • Troy Hunt, "Here's how I verify data breaches," 2016. https://www.troyhunt.com/heres-how-i-verify-data-breaches/
  • lexfo/stealer-parser. https://github.com/lexfo/stealer-parser
  • TreRB/combolist-check. https://github.com/TreRB/combolist-check (ingest report and line counter: src/valtik_combolist_check/ingest.py; silent-skip note: README, "Supported shapes" section)
  • thredb/sysinfo-parser. https://github.com/thredb/sysinfo-parser
Previous The most important file is the one that isn't there