v2.7.2–2.7.9: AI/ML detectors, impact-rich verification, mobile UX, resilience, caching — and eight bug fixes - #4
Merged
Conversation
Grounded in a real authorized-scope scan where an ElevenLabs key in a client-side EnvConfig.js was caught only by the generic catch-all — so it was both mis-typed (untyped MEDIUM) and double-counted. Added: 9 structural AI/ML provider detectors (54 -> 63) — ElevenLabs, Groq, Hugging Face, Replicate, Perplexity, xAI, OpenRouter, LangSmith, Pinecone. Prefix + fixed-length shapes keep them high-precision without the generic entropy gate; ElevenLabs carries provider-specific remediation. Fixed: one credential matched by both a typed detector and the generic catch-all produced two findings, because fingerprint includes secret_type. That double-counted exposures in client reports, spent a second AI-validation call per secret, and left two conflicting severities. _collapse_generic_ duplicates() now collapses per (source_url, raw_match), keeping the typed detector. The catch-all still fires for untyped credentials. Suite 282 -> 285, all green; ruff clean. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01V6XK23ZKmYtCovRte9a73E
Closes roadmap item R1 (verification depth). A verified credential no longer reports a bare "verified" — it reports what the key actually reaches, which is the concrete blast radius a client report needs. Adds seven AI/ML provider verifiers, pairing one with every safe detector from the v2.7.2 pack: ElevenLabs, Groq, Hugging Face, Replicate, OpenRouter, xAI, Pinecone. Verifier coverage 22 -> 29 secret types. For AI keys the quantified loss is usually spend, so verifiers surface plan tier and remaining quota where the provider exposes it: ElevenLabs · creator tier · quota 12,345/100,000 Hugging Face @acme-bot · role: write · 2 org(s) OpenRouter · key: prod-key · quota 42/500 xAI reports disabled keys with HTTP 200; the verifier reads api_key_blocked / api_key_disabled and returns unverified rather than claiming a dead key is live. Every verifier keeps the existing contract: exactly one read-only identity call to the credential's own issuer (never the scan target), no writes, no inference or generation calls (which would bill the victim's account), the secret never stored or returned, fail-closed on error. Still OFF by default behind VERIFY_SECRETS=true — authorized-scope only. Also corrects docs/TECHNICAL-AUDIT-AND-ROADMAP.md, which still listed R1 as an open gap after it shipped in v2.6.0. Suite 285 -> 294, all green; ruff clean. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01V6XK23ZKmYtCovRte9a73E
The dashboard is how a client first sees SecretNode, and it is increasingly opened on a phone. The findings table had nine columns, so on a phone the two things an operator actually needs — the severity badge and the DETAIL / FP buttons — sat off-screen behind horizontal scrolling. Findings table now becomes cards below 900px. Each row renders as a self-contained card and every cell is prefixed with its column name via a data-label attribute, so no information is lost and nothing scrolls sideways. The redundant row-number column is hidden, long source URLs and AI reasoning wrap instead of being ellipsis-clipped, and DETAIL / FP become full-width thumb targets. 900px rather than the phone breakpoint because a nine-column table is cramped well above phone width — this covers tablets in portrait. Touch sizing is keyed to pointer: coarse rather than screen width, since touch capability belongs to the input device, not the viewport: a tablet gets thumb-sized targets even at 900px while a mouse-driven desktop keeps its compact controls. Also fixes iOS auto-zoom (inputs render at 16px on touch; below that Safari zooms the page on focus and leaves the dashboard mis-scrolled), reflows the export toolbar into a two-column grid on small screens, and adds safe-area insets for notched phones. Verified with Playwright across iPhone SE / iPhone 14 / Pixel 7 / iPad mini / iPad Pro / desktop: zero page-level horizontal overflow at every width, no interactive control under 32px on touch, and the desktop table confirmed unchanged (thead still table-header-group, all nine columns intact). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01V6XK23ZKmYtCovRte9a73E
Being a good guest on a client's infrastructure is part of the engagement: an authorized scan that trips rate limiting looks like an attack to their SOC and gets the scanner blocked mid-assessment. Fixes a real bug first. RFC 7231 allows Retry-After to be either delta-seconds or an HTTP-date. The header was parsed with a bare float(), so a spec-compliant date raised ValueError, fell through to the generic exception handler, and made the scanner abandon the asset outright — a false negative caused by the server behaving correctly. _parse_retry_after() now handles both forms, clamps past dates to zero, falls back on garbage, and never raises. Adds jittered backoff. Retry delays used a deterministic RETRY_BACKOFF_BASE ** attempt, so every concurrent worker retried on the same tick — a thundering herd against a host that had just asked for relief. Backoff now uses equal jitter (half fixed, half random, capped), which de-synchronises workers while still guaranteeing a minimum pause, unlike full jitter. Adds an adaptive per-host throttle. On 429/503 SecretNode paces requests to that host only, growing per signal up to a cap and decaying as the host recovers. One fragile host no longer slows the rest of the engagement, and a healthy host costs nothing — pacing starts at zero and only appears after the host complains. Throttle state resets per scan so pacing learned from one target never penalises the next. This is resilience for authorized testing, not evasion. The identifiable-source posture is unchanged: SECRETNODE_USER_AGENT still presents a client-approved agent string, and scope, SSRF guard, passive-only behaviour and the authorization gate are untouched. Suite 294 -> 313, all green; ruff clean. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01V6XK23ZKmYtCovRte9a73E
Server-rendered apps embed bootstrap state in the HTML (__NEXT_DATA__, __NUXT__, __INITIAL_STATE__, __APOLLO_STATE__). That blob is built server-side, so it regularly carries config a developer never meant to ship. extract_inline_json_strings() finds inline <script> JSON, parses it, and scans the decoded string values. The roadmap listed inline JSON and HTML comments as uncovered surface. Measured against the code, both were already caught — the whole response body goes through the raw-text pass, so a plainly-embedded secret in a comment or JSON blob has always been found. The only genuine miss was a value whose JSON escaping breaks the credential's shape: a \uXXXX-escaped character mid-token, as emitted by XSS-safe serializers (Next.js's htmlEscapeJsonString). The regex sees sk-ant-… and no longer recognises it. Decoding recovers it — the same rationale that already justifies decoding source-map sourcesContent. The roadmap entry is corrected rather than left overstating the gap. Purely local decoding: no additional requests, so the scan stays passive. Bounded by a shared byte budget, non-greedy bounded regexes, defensive throughout — a malformed blob is skipped, never fatal. Deliberately not implemented: probing for unlinked paths (.env, .git/config, backups). That is active enumeration, not passive discovery, and would contradict the "passive assessment" statement in every client report. Noted in the roadmap as requiring a separate opt-in mode if ever added. Includes a mutation check: with SCAN_INLINE_JSON=false the recovery tests fail, proving they exercise the decoder rather than passing incidentally. Suite 313 -> 327, all green; ruff clean. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01V6XK23ZKmYtCovRte9a73E
Closes roadmap item R10. Re-scanning a target refetched every asset from
scratch — wasted bandwidth on the client's servers and wasted CPU on ours,
since most assets neither change between engagements nor contain anything.
SecretNode now sends If-None-Match / If-Modified-Since from a per-target cache
of HTTP validators (asset_cache table, 24h TTL).
A 304 is acted on by history, not blindly:
- asset was clean last scan -> skipped entirely (unchanged and previously
clean means still clean), and never enters the scan text;
- asset previously yielded a finding -> refetched unconditionally, so the
finding is reproduced. A finding that disappeared from a report would read
as "resolved", which is a dangerous lie to tell a client.
No response bodies are cached, deliberately. The obvious implementation stores
bodies so a 304 can still be scanned, but a client's JavaScript can contain live
credentials and caching it would leave a long-lived copy of their secrets on our
disk, which the engagement's confidentiality terms do not allow. The cache holds
only validators, a truncated content hash, and a clean/dirty flag — enough to
skip the overwhelming majority of assets. A test asserts no body content is ever
persisted.
Includes a mutation check (with ASSET_CACHE=false, five cache tests fail,
proving they exercise the real path) and a real-SQLite round-trip covering
upsert, per-target isolation and purge.
Suite 327 -> 337, all green; ruff clean.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01V6XK23ZKmYtCovRte9a73E
A deliberate self-review of v2.7.2-v2.7.7 before opening the PR. It found three genuine defects — two in the very code written to prevent that class of failure — and one process gap that mattered more than any of them. Fixed: - Inline-JSON extraction truncated at the first raw "<". The [^<] class cut the blob short whenever it contained a literal "<" (prose like "a < b", embedded HTML, templates), losing every secret after that point. Now lazily matched to the </script> terminator: linear-time, verified ReDoS-safe at 0.18s on hostile input. - A previously-dirty asset could be lost on 304 when RETRY_ATTEMPTS=1. The refetch used `continue`, consuming a retry attempt, so with one attempt configured the request was never re-issued and the asset was dropped — the exact "a finding silently vanishes" failure the cache exists to prevent. The unconditional refetch now happens inline. - An unprompted 304 with no cache entry burned a retry. Now refetched immediately, and a server answering 304 even unconditionally terminates rather than spinning. The quality gate is now real: - Corpus 27 -> 45 samples, covering all nine v2.7.2 AI/ML detectors, the v2.7.6 inline-SSR path, and eight hard negatives chosen to be structurally confusable with the new patterns (sk_ + wrong-length hex, a 64-hex SHA digest resembling an OpenRouter key, provider-shaped placeholders, benign inline JSON). Shipping nine detectors with zero corpus coverage meant the "measured precision" claim did not cover them. - make bench previously printed a report and always exited 0, so it could not block anything. It now enforces BENCH_MIN_PRECISION / BENCH_MIN_RECALL, names the offending samples, and exits non-zero. - CI runs it, so a precision regression is release-blocking. Verified by deliberately loosening the ElevenLabs regex: precision falls 1.000 -> 0.917, both false positives are named, build fails. Suite 337 -> 349, all green; ruff clean; bench 1.000/1.000 on the larger corpus. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01V6XK23ZKmYtCovRte9a73E
A full pre-PR QA pass: booted the application, ran real end-to-end scans against
a local target with planted secrets, exercised every export format, the CLI, and
a re-scan to prove the cache. It surfaced one serious defect and one cosmetic
one.
Client reports contained the full, live credential. The CSV column was named
matched_value_partial but wrote raw_match verbatim, and the HTML report did the
same. A report is emailed, forwarded and archived — writing a working secret
into one turns the deliverable itself into a second exposure, and directly
contradicts the Rules of Engagement we ask clients to sign ("only the minimum
evidence needed to prove a finding, with sensitive data redacted"). Values are
now redacted to sk_798…******…3fc4 (51 chars): still greppable so a developer
can identify which key to rotate, but not usable. REPORT_FULL_SECRETS=true opts
back in when an operator deliberately needs the full value. SARIF was clean.
Version drift in the dashboard: the footer and boot log were hardcoded to v2.7.1
and never touched by the runtime /api/health sync, so a client demo showed a
stale version indefinitely. Footer is now synced, boot log no longer hardcodes a
version, and report.py's fallback constant was refreshed.
Verified end to end rather than only unit-tested: app boots and reports the
right version; a real scan found both of this session's headline features
working live (v2.7.2 ElevenLabs detector, and v2.7.6 recovery of an Anthropic
key \uXXXX-escaped inside __NEXT_DATA__ with a raw "<" in the blob, exercising
the v2.7.8 fix); a re-scan repopulated findings rather than losing them and
recorded both assets as was_clean=False; HTML/CSV/SARIF all generate with the
right version and 63 rules; the CLI works; the SSRF guard correctly refused a
localhost target until ALLOW_PRIVATE_TARGETS was set; dashboard JS passes
node --check after the mobile edits.
Suite 349 -> 353, all green; ruff clean; bench 1.000/1.000.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01V6XK23ZKmYtCovRte9a73E
azmolhaque
force-pushed
the
claude/new-session-thsmic
branch
from
July 30, 2026 08:30
26bd903 to
744491d
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Eight sequenced releases taking SecretNode from v2.7.1 → v2.7.9. Each is a self-contained, tested slice rather than one sweeping rewrite — the working tool stayed green throughout.
The work was grounded in a real authorized-scope scan: an ElevenLabs key shipped in a client-side
EnvConfig.jswas caught only by the generic catch-all, so it was both mis-typed (untyped MEDIUM) and double-counted. Closing that one gap surfaced most of what follows.Releases
Eight bugs fixed
Three were found by reviewing this session's own work, and the two most serious would not have been caught by any test:
matched_value_partialbut wroteraw_matchverbatim; HTML did the same. A report is emailed, forwarded and archived — writing a working secret into one turns the deliverable itself into a second exposure, and contradicts the Rules of Engagement clients sign. Nowsk_798…******…3fc4 (51 chars)— greppable enough to identify which key to rotate, not usable.Retry-Afteras an HTTP-date silently dropped the asset. RFC 7231 allows a date; a barefloat()raisedValueError, the generic handler swallowed it, and the asset was abandoned — a false negative caused by the server behaving correctly.fingerprintincludessecret_type, so a value matched by both a provider detector and the generic catch-all double-counted the exposure, spent a second AI-validation call on it, and left two conflicting severities.<— losing every secret after it in blobs containing prose like"a < b"or embedded HTML.RETRY_ATTEMPTS=1— the refetch consumed a retry attempt, so the asset was dropped. This was the exact "a finding silently vanishes" failure the cache exists to prevent.The detection quality gate is now real
make benchpreviously printed a report and always exited 0, and CI never ran it — so detection quality was neither measured nor enforced, and nine new detectors shipped with zero corpus coverage.sk_+ wrong-length hex, a 64-hex SHA digest resembling an OpenRouter key, provider-shaped placeholders).make benchenforcesBENCH_MIN_PRECISION/BENCH_MIN_RECALL, names offending samples, exits non-zero.Proven end to end: deliberately loosening the ElevenLabs regex drops precision 1.000 → 0.917, names both false positives, and fails the build.
Verified in a running system, not only unit-tested
/api/healthreports the right version; dashboard serves.\uXXXX-escaped inside__NEXT_DATA__with a raw<in the blob (exercising the 2.7.8 fix).asset_cacherecorded both assetswas_clean=False.ALLOW_PRIVATE_TARGETSwas explicitly set.node --checkafter the mobile-layout edits.Two things deliberately not built
SECRETNODE_USER_AGENTstill presents a client-approved agent string..env,.git/config, backups). That is active enumeration, not passive discovery, and would contradict the "Passive assessment" statement in every client report. Noted in the roadmap as requiring a separate, clearly-labelled opt-in mode if ever added.Privacy note: the asset cache stores no response bodies — only validators, a truncated content hash, and a clean/dirty flag — because client JavaScript can contain live credentials. A test asserts no body content is ever persisted.
Type of change
Checklist
ruff check backend/passespytestpasses (added/updated tests for the change) — 353 tests, all greenseverity,cwe, andremediationVERIFY_SECRETS🤖 Generated with Claude Code
https://claude.ai/code/session_01V6XK23ZKmYtCovRte9a73E
Generated by Claude Code