Artifact review: stop the credential at the API boundary (v2.8.0) - #5
Merged
Conversation
Driven by reading the artifacts of a real v2.7.9 deep scan side by side — the dashboard modal, the Discord alert, and the SARIF/CSV/HTML of the same three findings. v2.7.9 fixed redaction in the reports; comparing the surfaces showed how much of that job was left undone everywhere else, and turned up two correctness bugs that had nothing to do with display. A re-scan of an unchanged site reported CLEAN while the credential was still exposed. The asset cache treated a 304 on the root page as "unchanged and previously clean, skip", but a page is a link graph — skipping its body meant never parsing its <script> tags, so every 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. Crawled pages now always return a body; the conditional GET is still sent, so unchanged terminal assets still skip. The dashboard rendered the full credential. redact_secret() landed in report.py only, so the REST API and WebSocket shipped raw_match verbatim and the modal displayed it under a "MATCHED VALUE (PARTIAL)" heading a 51-character key fits inside. Masking now happens at the API boundary with no opt-out; REPORT_FULL_SECRETS still works for a report an operator deliberately generates. Capturing a live WebSocket stream also caught scan_complete shipping both unmasked finding lists in the final frame of every scan, and the code snippet carrying the secret the matched-value field was hiding. The JSON export was the one deliverable format still returning the stored record as-is. The dashboard showed MEDIUM for findings the reports called HIGH: severity was re-derived in the frontend from a hardcoded list of 14 type names, so every detector added since — the whole AI/ML provider family — hit the MEDIUM default. It now reads the field the backend already computes. Discord had the same shape of bug twice: a version literal stale since 2.4.0, and a per-type colour table covering 16 of 60+ detectors. Deep-scan reports lost every scan-level metric, which is why a 25-host SARIF claimed assets_fetched: 0. Rolled up, with duration as wall-clock rather than summed across concurrent hosts. Asset counting now separates downloaded from cached-but-covered, so a fully-cached re-scan no longer reports "0 assets analysed". Deep scans also start from an empty asset cache instead of inheriting module state from whatever ran before them. Suite 347 -> 382, including a mutation check that the leak guards can fail and a reproduction of the false all-clear. Lint clean, detection benchmark 1.00/1.00. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01V6XK23ZKmYtCovRte9a73E
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
Driven by reading the artifacts of a real v2.7.9 deep scan side by side — the dashboard modal, the Discord alert, and the SARIF/CSV/HTML exports of the same three findings. v2.7.9 fixed redaction in the reports; comparing the surfaces showed how much of that job was left undone everywhere else, and turned up two correctness bugs that had nothing to do with display.
The two that produce a wrong answer, not a wrong presentation
A re-scan of an unchanged site reported CLEAN while the credential was still exposed. The asset cache treated a
304 Not Modifiedon the root page as "unchanged and 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 against a lab target with a planted key: scan 1 found it, scan 2 never requested the file holding it and reported no findings. Crawled pages now always come back with a body (allow_cache_skip=False); the conditional GET is still sent, so the bandwidth saving on unchanged terminal assets (JS bundles, source maps) is untouched.Deep scans inherited asset-cache state from whatever ran before them in the same process. The cache is module-level and only the single-target endpoint primes it, so a deep scan could report a previous scan's cache hits as its own coverage — and act on a stale validator. Deep scans now start from an empty cache, and
run_scanresets the hit tally alongside the throttle.The credential-exposure set
redact_secret()was added toreport.pyin v2.7.9 and nowhere else, so/api/scans/{id}and the WebSocket stream shippedraw_matchverbatim and the finding-detail modal displayed it — under a heading reading "MATCHED VALUE (PARTIAL)" that a 51-character key comfortably fits inside. Redaction now happens at the API boundary (public_scan/public_event) using amask_secret()with no opt-out.REPORT_FULL_SECRETSremains available for a report an operator deliberately generates, but no longer unmasks every dashboard session and WebSocket subscriber.scan_completeevent carried both unmasked finding lists. Found by capturing a live WebSocket stream rather than by reading the code: per-finding events were being scrubbed, but the final frame of every scan ships the entire result dict.context_snippetwas stored and served verbatim. Now masked inValidatedFinding.to_dict(), where the complete value is still available to match against — masking downstream from the 80-characterraw_matchcap would leave the tail of a longer secret exposed.format=jsonreturned the stored record as-is.The consistency set
severityfield the backend already computes from the pattern registry. A missing.badge-lowstyle meant LOW findings rendered unstyled.backend/version.py; embed colour is keyed on effective severity, and severity is stated as a field.DeepScanResult.to_dict()never emittedassets_fetched, which is why a SARIF from a 25-host run claimedassets_fetched: 0; the same omission droppedraw_findings,duration_seconds, posture findings and the verification counts. All rolled up, with duration measured as wall-clock rather than summed across concurrently scanned hosts.assets_fetched/assets_cached/assets_scanned; reports lead with coverage and name the cached portion.matched_value_partial,found_atand the originatinghostso a triager can correlate against the CSV row and the Discord alert.Type of change
Checklist
ruff check backend/passespytestpasses (added/updated tests for the change) — 347 → 382severity,cwe, andremediation— n/a, no new detectorsVerification
Beyond the unit suite, this was exercised against a local lab target with a planted, correctly-shaped key — which is how three of the eight bugs were found in the first place:
The finding survives re-scans (no false all-clear), the clean unchanged bundle is still skipped (the cache optimisation is intact), and the key appears in none of: the WebSocket stream,
/api/scans/{id}, or the JSON/CSV/SARIF/HTML exports.The leak guards are mutation-tested — each fix was reverted and the corresponding tests confirmed to fail — because a redaction test that passes with redaction switched off is testing nothing.
Detection benchmark: precision 1.000, recall 1.000, quality gate PASS.
🤖 Generated with Claude Code
https://claude.ai/code/session_01V6XK23ZKmYtCovRte9a73E
Generated by Claude Code