Skip to content

v2.8.1 — The credential stops at the API boundary, and the mask tells the truth

Latest

Choose a tag to compare

@azmolhaque azmolhaque released this 04 Aug 11:11
· 2 commits to main since this release
2361a2d

Two releases in one tag. v2.8.0 was never published, so its notes are folded in here.

Both came from the same exercise: reading the artifacts of a real deep scan side by side — the
dashboard modal, the Discord alert, and the SARIF/CSV/HTML exports of the same three findings —
and asking which of them a client could actually act on.

The one that produced a wrong answer

A re-scan of an unchanged site reported CLEAN while the credential was still exposed. The
asset cache treated 304 Not Modified on the root page as "unchanged, previously clean, skip".
But an HTML page is a link graph, not just something to grep. Skipping its body meant never
parsing its <script> tags, so every JS bundle it referenced dropped out of the scan.

Reproduced on a lab target: scan 1 found a planted key, scan 2 never requested the file holding
it and reported no findings. Crawled pages now always return a body; the conditional GET still
goes out, so the bandwidth saving on unchanged terminal assets is untouched.

Redaction happens where it cannot be skipped

redact_secret() landed in report.py and nowhere else, so five surfaces still carried live
credentials:

  • the dashboard, which rendered the full key under a heading reading "MATCHED VALUE
    (PARTIAL)" that a 51-character key fit inside
  • the scan_complete WebSocket frame, which ships the whole result dict — found by capturing
    a live stream, not by reading code
  • the code snippet, which leaked the secret the matched-value field was hiding
  • the JSON export, the last deliverable format still returning stored records as-is

Masking now happens at the API boundary with no opt-out. REPORT_FULL_SECRETS still works for a
report an operator deliberately generates, but no longer unmasks every dashboard session and
WebSocket subscriber.

The mask itself was lying (v2.8.1)

Fixing where masking happens exposed a problem with what reached it. Storage capped the
matched value at 80 characters with value[:80] + "…", and the mask then reported the length and
last four characters of that capped string. Every credential longer than 80 characters
surfaced as:

ghp_AA…******…AAA…  (81 chars)

A fabricated length — always exactly 81 — and a "tail" made of padding and the ellipsis itself.

Both halves of a mask exist to identify a credential without exposing it, and both were useless
for precisely the secrets where identification matters most: GCP service-account keys, JWTs, PGP
blocks, database URIs with long passwords. Two different 100-character tokens on the same host
rendered as the same string, so a triager could not tell them apart, and an engineer told to
rotate "the 81-character key" had nothing to search for. It now reads:

ghp_AA…******…TAIL  (112 chars)

The cap keeps the head and the real tail inside the same budget, and the true length travels
beside it. Every report surface goes through one helper, so the dashboard, CSV, SARIF, HTML and
JSON cannot disagree about how large a key is.

Also fixed

  • The dashboard showed MEDIUM for findings the reports called HIGH. The frontend re-derived
    severity from a hardcoded list of 14 type names, so every detector added since — the entire
    AI/ML family — fell through to the MEDIUM default.
  • Discord alerts announced "v2.4.0" for five releases, and coloured every embed from a table
    covering 16 of 63 detectors, so an ElevenLabs key and an AWS root key arrived looking identical.
  • Deep-scan reports lost every scan-level metric — a SARIF from a 25-host run claimed
    assets_fetched: 0. Duration is now wall-clock rather than summed across concurrent hosts.
  • A fully-cached re-scan reported "0 assets analysed" — true as a download count, wrong as
    coverage. Split into fetched / cached / scanned.
  • Deep scans inherited asset-cache state from whatever ran before them in the same process.

Known limitation, stated rather than hidden

REPORT_FULL_SECRETS cannot reveal a credential longer than the storage cap, because the full
value is deliberately never persisted. Its purpose is handing a key to an engineer for rotation,
which for a long key it cannot do. Lifting that means storing credentials verbatim — the thing
this release set out to stop — so it is recorded as a trade-off for the operator rather than
quietly changed.

Tests

41 new cases across the two releases, including mutation checks that the leak guards and the cap
fix can actually fail, and a reproduction of the false all-clear. Suite: 347 → 388. The
detection-quality gate holds at precision 1.000 / recall 1.000 on the labelled corpus.


Since v2.6.0

The 2.7 line, which never got its own releases:

2.7.0 Deep attack-surface platform — concurrent multi-host passive scanning
2.7.1 Gemini validation-engine model refresh
2.7.2 AI/ML provider detector pack — 54 → 63 patterns
2.7.3 Seven AI/ML provider verifiers, impact-rich verification
2.7.4 Mobile and tablet UX for the live dashboard
2.7.5 Scan politeness and resilience
2.7.6 Inline JSON/SSR state blobs decoded before scanning
2.7.7 Conditional-GET asset caching for re-scans
2.7.8 Three bugs, and a detection-quality gate that can actually fail
2.7.9 Client reports no longer contain the live credential

Authorized use only. SecretNode is passive: it never uses a discovered credential to prove it
is live, and never writes to a target