Skip to content

Feat/m5 corpus - #9

Merged
mkmuniz merged 4 commits into
mainfrom
feat/m5-corpus
Oct 1, 2026
Merged

mkmuniz merged 4 commits into
mainfrom
feat/m5-corpus

Conversation

@mkmuniz

@mkmuniz mkmuniz commented Oct 1, 2026

Copy link
Copy Markdown
Owner

What this changes

Lands the first third of M5: a labeled precision/recall corpus with its own test gate, a standardized mask separator across every document type, and vedei's first secret rule of its own — AWS access key ids, which the upstream corpus misses entirely.

Why

A scanner's worth is precision and recall on real input, and until now neither was measured. Three things follow from being able to measure them:

The corpus. For Brazilian data, precision is the hard axis: the upstream secret rules score candidates against an English dictionary, so senhapadrao reads as random and gets reported. That cost is invisible without labeled data, so there was no way to tell whether a change made the tool noisier. Now there is.

The masks. Two divergences in how document types tolerated separators. CNS and título used [\s.], and \s matches a newline, so either matched a value split across two lines — the exact bug the card pattern was already written to avoid. And CPF, CNPJ and PIS rejected a space outright, so 529 982 247 25 was missed while a space-separated CNS was caught.

AWS. Measured, not assumed: betterleaks reports nothing for an AWS access key id — not bare, not with an AWS_ACCESS_KEY_ID= context, not for ASIA, not even the 40-char secret key. An access key id in a log or an agent transcript is exactly what vedei exists to keep out of a model's context.

Closes #

Checklist

  • make test passes, including -race
  • make lint reports zero issues (plus gosec clean on the new detect/aws and corpus packages)
  • New code does not lower coverage — detect/aws and corpus arrive with their own suites

If this adds or changes a detector

This touches two: the mask change in detect/br, and the new detect/aws.

  • Pure, std-lib only. detect/aws has the same zero-dependency shape as detect/br
  • Property-based tests — the mask change rides on the existing check-digit fuzzers (FuzzCPFCheckDigits and the rest), which generate valid values and corrupt them. The space/tab masks are asserted equivalent in TestExtract_SpaceAndTabAreUniformSeparators. AWS has no check digit to fuzz, so it is covered by example from both sides — valid prefixes and bodies pass, non-AWS prefixes and non-base32 bodies fail
  • Edge cases covered, and the PR explains below why these are the ones that matter
  • corpus/ entries, both kinds — this is the PR that makes that checklist line real. Space-separated CPF as a true positive, a three-digit table column as the false positive it must not become; two AWS keys as true positives, a non-AWS-prefix lookalike as the reject
  • Confidence reflects what is proven. AWS is High confidence (prefix + base32 is specific) and Unknown validity (well-formed, not confirmed live), and the un-verified account-id checksum is noted in the code rather than invented

The edge cases that matter:

  • Mask: a three-column table of three-digit numbers is the feared false positive from admitting a space (100 200 300 40). The check digit a column does not satisfy is what keeps it quiet — corpus/cases/cpf.txt has it.
  • AWS: base32 has no 0, 1, 8 or 9, so AKIA1KJ7WQZR2LMNXBTQ is rejected — a real structural check, not just a 20-char shape.

If this touches anything a detected value passes through

detect/aws produces findings that flow through the same report, redaction and fingerprint paths as every other.

A scanner's worth is precision and recall on real input, not that it compiles,
and for Brazilian data precision is the hard one: the upstream secret rules
score candidates against an English dictionary, so "senhapadrao" reads as random
and gets reported. That cost is invisible without labeled data. This is the data,
and the test that gates on it.

Format is one directive per line: "expect <type> <value>" for a true positive,
"reject <value>  # reason" for a false positive. The reason is required on a
reject, because a false-positive fixture nobody can explain is one nobody can
safely remove. The parser rejects a malformed line rather than skipping it — a
corpus that silently drops a case reports a precision it did not measure.

The gate is strict: every expect found, every reject quiet. That is right for
hand-labeled data where each line is a deliberate claim; a threshold is for
scraped corpora. Precision and recall are printed regardless, because the
numbers are the point and should be seen to move.

Writing the fixtures already earned its keep: two were mislabeled and the
harness caught both. 00.000.000/0001-91 is the Uniao's real, valid CNPJ, not the
reject I wrote; and a +55 phone is correctly a pix-key now, not the card-pan it
used to be read as. Both are the kind of error that only shows up against labeled
data.

cases/false-positives.txt is the running record of what scanning the real world
produced — the Go pseudo-version timestamp, the UUID, the phone — each one a
finding that was once wrong. 31 cases today, precision 1.000, recall 1.000.
… newlines

Two divergences in how document types tolerated separators, resolved into one
rule: a group boundary accepts the document's own punctuation, a space, or a
tab, and never a newline.

The first divergence was a latent bug. CNS and título used "[\s.]", and \s
matches a line break, so either could match a value split across two lines — the
exact failure the card-number pattern was already written to avoid. Now none of
them can.

The second was an inconsistency the architecture notes had flagged and deferred:
CPF, CNPJ and PIS rejected a space outright, so "529 982 247 25" was not caught
while a space-separated CNS was. The worry about admitting a space was that it
matches any three-column table of three-digit numbers. That worry is now
measured instead of guessed — corpus/cases/cpf.txt has the table-column case,
and the check digit a column does not satisfy keeps it quiet. Precision stays
1.000 across 36 cases, recall 1.000.

The deferred note in ARCHITECTURE.md is replaced with what was decided and why.
…s misses

betterleaks reports nothing for an AWS access key id — not bare, not with an
"AWS_ACCESS_KEY_ID=" context, not for ASIA. Measured, not assumed: a probe
against it returned zero for every shape, including the 40-char secret key. An
access key id in a log or an agent transcript is exactly what vedei is meant to
keep out of a model's context, so this is the one detection rule vedei owns
outside the Brazilian core.

It is structural, like a CPF whose check digits close: a credential-bearing
prefix (AKIA long-term, ASIA temporary) followed by sixteen base32 characters.
base32 has no 0, 1, 8 or 9, so a 20-char run with one of those in the body is
ruled out for free — that is a real check, not just a shape. Reported High
confidence, because the prefix-plus-base32 combination is specific, and Unknown
validity, because confirming a live key means calling AWS (M9). AIDA, AROA and
the other prefixes are IAM resource ids, not secrets, so they are left out
rather than add noise.

This is a deliberate exception to ADR-001. betterleaks stays the only *imported*
corpus — the detector is vedei's own, pure, std-lib only — and the exception is
there because the gap is real and corpus/cases/secrets.txt proves it: those
expect-cases failed before this and pass after. The account-id checksum an
access key id carries is noted for a later revision rather than invented now.

The 40-char secret access key has no structure to key on and is left to
context-based rules, a gap stated in SECURITY.md. .vedeiignore gains the corpus
and test fixtures so the self-scan stays clean. Corpus: 40 cases, precision and
recall 1.000.
The structure block had two report/ lines and no detect/aws/; fixed, and corpus/
now describes what it is — a labeled precision/recall harness with its own make
target — rather than an aspiration. M5 moves to in-progress in both READMEs: the
corpus and the first detection fixes have landed, the pt-BR dictionary
calibration that is the heart of M5 has not.
@mkmuniz mkmuniz self-assigned this Oct 1, 2026
@mkmuniz mkmuniz added bug Something isn't working documentation Improvements or additions to documentation enhancement New feature or request labels Oct 1, 2026
Comment thread corpus/cases/secrets.txt Dismissed
Comment thread corpus/cases/secrets.txt Dismissed
Comment thread corpus/cases/secrets.txt Dismissed
Comment thread corpus/cases/secrets.txt Dismissed
@mkmuniz
mkmuniz merged commit c111e4e into main Oct 1, 2026
16 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working documentation Improvements or additions to documentation enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants