Skip to content

perf + fix(detectors): 2.5x faster scans and false-positive reductions - #96

Merged
pixincreate merged 2 commits into
masterfrom
perf/memory-and-false-positives
Sep 5, 2026
Merged

perf + fix(detectors): 2.5x faster scans and false-positive reductions#96
pixincreate merged 2 commits into
masterfrom
perf/memory-and-false-positives

Conversation

@pixincreate

Copy link
Copy Markdown
Owner

Summary

Three work streams, measured end to end:

Memory

  • Structural audit: no mem::forget/Box::leak/static growth/Rc cycles anywhere in src/ — nothing can leak by construction.
  • Empirical: macOS leaks cannot attach here (hardened-runtime restriction), so bounded memory is proven by measurement instead: scanning a 1 GB file peaks at ~24 MB RSS (the stream never holds the file).
  • One noted (not fixed) edge: peak RSS is proportional to retained findings — a pathological file producing ~2M findings held ~500-800 MB of report data. Capping findings is a design decision, not a leak.

False positives (16 → 2 on a curated FP corpus)

  • Aadhaar: new validate = "verhoeff" (dihedral D5 tables) — real Aadhaar numbers carry the check digit; a UUID tail reported HIGH before.
  • AWS documentation example key/secret: exact-match allowlisted.
  • Placeholder values (changeme, your-api-key-here, replace-me-please): allowlisted in Password/GenericKeyValue detectors — case-sensitive and shape-checked, so mixed-case values stay reported.
  • Emails: RFC 2606 example domains and noreply conventions allowlisted.
  • Phone numbers: the fictional 555 exchange allowlisted.
  • Checksums: npm/shield sha256-/sha384-/sha512- prefixes allowlisted in RandomString.
  • Residual 2: a company-domain email (indistinguishable from PII) and a bare 43-char integrity value whose algorithm prefix sits outside the match.

Speed (1 GB clean scan: 18.0 s → 7.2 s, ~57 → ~143 MB/s)

Profile showed 42% of runtime in N separate regex passes for the keywordless detectors and 13% in Unicode lowering:

  • One combined any-of regex now gates all keywordless detectors — is_match is existence, so the gate is exact (no match lost); each pattern is group-isolated so flags cannot leak between branches. Matching lines pay the gate plus their real passes; non-matching lines skip every individual pass.
  • to_lowercase_into takes a byte-per-byte ASCII fast path.
  • shannon_entropy uses a fixed byte histogram — no HashMap allocation per candidate match.

Tests

216 pass (Verhoeff unit tests against the canonical 2363/2362 examples + a new detector_tests case for the 555 allowlist; FP-corpus-affected fixtures updated to detected-format equivalents). fmt/clippy clean; self-scan clean and drift-free.

…ists

Measured on a corpus of common false-positive-prone content (READMEs
with documentation emails, .env.example placeholders, docker-compose,
npm integrity strings, UUIDs, fictional phone numbers): 16 findings, 11
of them false positives. This change eliminates 14 of the 16.

- Aadhaar numbers carry a Verhoeff check digit; the new validate =
  "verhoeff" rejects random 12-digit runs (a UUID tail reported HIGH).
  Real-Aadhaar fixtures replaced with Verhoeff-valid numbers.
- AWS's documentation example key/secret are exact-match allowlisted —
  they appear in READMEs everywhere and are never real.
- Placeholder values (changeme, your-api-key-here, replace-me-please)
  are allowlisted in Password/GenericKeyValue detectors, case-sensitively
  and shape-checked so mixed-case values stay reported.
- RFC 2606 example-domain emails and noreply conventions are allowlisted.
- The fictional 555 exchange is allowlisted for phone numbers.
- npm/shield checksum prefixes (sha256-/sha384-/sha512-) are allowlisted
  in RandomString, whose match text includes the prefix.

The committed baseline shrank accordingly (those findings no longer
exist). Corpus: 16 -> 2 findings, both inherent (a company-domain email
indistinguishable from PII, and a bare 43-char integrity value whose
algorithm prefix sits outside the match).
@pixincreate
pixincreate force-pushed the perf/memory-and-false-positives branch from 8b20a21 to efdf3e3 Compare September 5, 2026 14:14
…tropy

Profile of a 1GB scan: 42% of runtime was N separate regex passes for
the keywordless detectors, 13% was Unicode char-by-char lowering.

- The keywordless detectors now share one combined any-of regex as an
  exact prefilter: lines that cannot match any of them skip all of their
  individual passes; is_match is existence, so nothing is lost, and each
  pattern is group-isolated so flags cannot leak between branches.
- to_lowercase_into takes a byte-per-byte ASCII fast path.
- shannon_entropy uses a fixed byte histogram instead of allocating a
  HashMap per candidate match.

Measured: 1GB clean scan 18.0s -> 7.2s (~57 -> ~143 MB/s), RSS unchanged
(~24MB, bounded); 112MB match-heavy 8.9s -> 7.5s.
@pixincreate
pixincreate force-pushed the perf/memory-and-false-positives branch from efdf3e3 to aeeea4c Compare September 5, 2026 14:17
@pixincreate
pixincreate merged commit d5732c2 into master Sep 5, 2026
7 checks passed
@pixincreate
pixincreate deleted the perf/memory-and-false-positives branch September 5, 2026 14:19
pixincreate added a commit that referenced this pull request Sep 5, 2026
- Verhoeff: canonical examples, separators, embedded id runs, degenerate
  inputs (the p-permutation breaks the all-zero identity chain).
- Luhn: known cards, separators, sub-13-digit rejection.
- shannon_entropy: information-theory identities, hex near-ceiling,
  multi-byte input.
- is_multiline: (?s), grouped (?s:...), combined (?is), negative flags,
  plain patterns — and a real fix the test caught: combined flags like
  (?is) missed the substring check, so such patterns were scanned
  line-by-line and silently missed multiline secrets. is_multiline now
  parses flag groups.
- Every FP allowlist entry exercised in both directions through the real
  detector set (AWS example key, placeholders, example-domain emails,
  555 numbers, checksum prefixes).
- The false-positive corpus is committed under tests/fixtures and scanned
  end to end: exactly the two documented residuals may appear.
- Finding derives Deserialize with a wire-format round-trip test: the
  historical plugin_name key and the detector_name alias both parse.
- git-history mode gets a hostile-git-config test mirroring the staged
  one (pins GIT_DIFF_FRAMING_ARGS coverage for log).
- stdin scans through NUL bytes; the secret after a NUL line is found.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant