fix(recon): close silent false-negatives + ledger completeness gaps (11 fixes)#1
Merged
Merged
Conversation
…11 fixes) Audit of the recon engine surfaced one critical silent false-negative plus a set of completeness/robustness gaps. All fixes are test- or run-verified (80 -> 88 tests). CRITICAL - SKIP_DIRS now matched RELATIVE to the scan root (new base.path_in_skip_dir). routes._in_skip_dir / scanners._in_skip_dir split the ABSOLUTE noir/scanner path and checked every segment, so a repo living under a skip-named ANCESTOR (.claude/worktrees/<id>, vendor/, target/, build/, out/, ~/.cache, security/) had its ancestor segment match and EVERY route + finding silently dropped -> the tool reported the app as empty / "nothing wrong". Recurrence of bug-005 in two post-filters the original fix missed. Proven: identical fixture -> 2 routes at a clean path, 0 under a `target/` ancestor; now 2 at both. Fails OPEN when a path can't be made relative. HIGH (completeness on large / scan-heavy repos) - Findings ledger consumes the FULL ranked static set (`all`), not the briefing's top-15 slice -> no HIGH/CRITICAL CVE/secret dropped at rank #16+ from ledger + calibration. - 12k-file walk truncation surfaced (files_truncated in FACTS + CLI warning + briefing banner) instead of silently analyzing an arbitrary subset. - endpoint_guards cap 400 -> 5000 WITH disclosure (endpoint_guards_truncated) so the missing-auth ledger isn't silently capped on big monorepos. MEDIUM - Unverified inbound webhooks now enter the ledger (new webhook-forgery class, CWE-345); previously surfaced only in the briefing, never ranked/calibrated. - integrations.SIG_VERIFY no longer matches the bare word `signature` (a comment like "no signature verification" suppressed the finding); keeps crypto/header/verb-prefixed patterns. Found during validation. - Dynamic probe caps (50/60/80) now report endpoints_over_cap in their summaries. LOW / robustness / polish - cross_tenant_bola coerces str(tenant) -> no crash on numeric tenant ids. - noir-present-but-empty now falls back to the regex pass (no silent route blind spot). - findings._sql counts sql-orm / prisma(sql) labels -> SQL-ORM+Mongo apps not mis-down-ranked. - Dockerfile pins the Trivy installer to a tagged ref + version (drops mutable-main | sh). - __init__.py docstring lists the real 7 run artifacts. Tests: +8 regressions (skip-dir-relative-to-root, normalize keeps findings under a skip-named ancestor, full static set into ledger, webhook->ledger, SIG_VERIFY tightening, numeric tenant id, SQL-ORM datastore vocab). 88 tests green; byte-compile + end-to-end pipeline verified. NOTE: the Dockerfile Trivy pin follows the documented installer interface but was not docker-build-verified in this environment — smoke-test with `docker build` before relying on it. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
raccioly
added a commit
that referenced
this pull request
Jun 10, 2026
…k CWE mislabel (#2) Follow-up to PR #1 (audit items deliberately deferred from the first batch). 1. dynamic.write_auth_enforcement treated HTTP 500 as "no-auth-gate (reached handler/validation)". A 500 is ambiguous — it can be the AUTH layer itself throwing, not the handler running unauthenticated — so it must NOT escalate to a HIGH missing-auth finding, and (worse) it was being recorded by calibration.samples_from_dynamic as a CONFIRMED-real missing-auth sample, poisoning the self-improving oracle. 500 now falls to the inconclusive `http-500` verdict, matching the forged-token engine which already excludes 500 from "reached handler". 2. Attack-surface sinks cited the wrong CWE. surface.py emits the key `sql-injection` but STANDARDS keys it `sqli`, so SQLi sinks fell back to attack_class `sast` (CWE-710 generic) instead of `sqli` (CWE-89). Added a `_SINK_ATTACK` alias (sql-injection -> sqli) and STANDARDS + REMEDIATION entries for nosql-injection (CWE-943), redos (CWE-1333), and eval-injection (CWE-95) so every sink class cites its specific CWE with a concrete fix. Tests: 500-inconclusive (+ oracle records nothing) with a 400-still-no-auth-gate regression guard; sink classes map to their specific CWE + a non-default remediation. 88 -> 91 tests, all green; end-to-end pipeline verified. Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
raccioly
added a commit
that referenced
this pull request
Jun 10, 2026
…tection classes
The retest on the same app both confirmed 0.3.0's catches and exposed where it was
wrong or incomplete. This corrects the former and adds the latter. 11 -> 15 extractors.
Two false positives 0.3.0 shipped (corrected — the same scrutiny applied inward):
- graphql.py: AppSync introspection IS disablable engine-level
(`introspectionConfig: IntrospectionConfig.DISABLED`) — recognize it and flag ONLY
when not set, instead of always crying wolf with "can't be disabled" advice.
- iac_ci.py / client_integrity.py: API_KEY default is anonymous/over-permissive auth
(missing-auth), NOT CSWSH. CSWSH needs ambient-cookie WS auth — now a real client-side
determinant (withCredentials on a WebSocket), so the tool can ANSWER the flag, not guess.
Also: a WAF byte-match on an app-layer token (`__schema`, SQL) is flagged as a bypassable
band-aid, never a fix.
Four new classes (each with the retest's static signal + a probe, vuln-fires/fix-clean tests):
- surface.py: redirect-SSRF — an outbound client that follows redirects (axios/requests do
by default) with no per-hop guard re-validates only hop 0 (#1's actual residual).
- upload_security.py (new): unrestricted upload (deny-list-only, stored-name-from-filename,
trust-client-MIME, accept-SVG) + serve-side stored XSS (no `nosniff`) (#2b).
- policy_consistency.py: password REUSE/history — hashing a new password with no comparison
to current/previous (the control #6 was actually about, distinct from complexity).
- pii_exposure.py (new): unmasked PII at the output boundary — `res.json(rawEntity)` + a
masking control defined but with ZERO live call sites; verify by VALUE SHAPE not field
name (catches indirect carriers like a phone embedded in a composed id) (#8).
Plus: findings ledger gains the new attack classes (CWE/ASVS/OWASP-API + remediation), 3 new
probe drafts (upload matrix + fetch-back, value-shape PII diff, reuse on every set-password path),
briefing/methodology surface it all, and the README §11 retest meta-lessons ("a WAF is never the
remediation"; "read the finding precisely"; "assert by value shape"; "validate the whole chain").
Verification: 92 tests (12 new, incl. FP-gone regressions) on py3.12+3.13; wheel builds + installs
+ smoke-runs (0.4.0, calibration + rules ship); self-scan dogfoods clean (1 Dockerfile LOW, 30
self-referential FPs suppressed); real CLI run confirms a fixed AppSync produces ZERO introspection
findings and all 4 new classes reach the ledger.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
raccioly
added a commit
that referenced
this pull request
Jun 10, 2026
…ative) that 0.4.0 missed 0.4.0 was tagged from a STALE local main and published to PyPI WITHOUT the two fixes already merged to origin/main (#1, #2) — most importantly #1's CRITICAL fix where a repo living under a skip-named ANCESTOR dir had every route + finding silently dropped (the tool reported a vulnerable app as clean). This rebases the 0.4.0 retest work onto #1 + #2 and ships the complete set to PyPI. - #1: skip-dir matched relative to the scan root (no silent empty-scan), full ranked static set into the ledger, walk-truncation disclosure, webhook-forgery -> ledger; +8 regressions. - #2: HTTP 500 no longer escalated to missing-auth (nor recorded as a confirmed oracle sample); sink classes cite their specific CWE (sqli / nosql / redos / eval), not generic sast. - 0.4.0 retest work (15 extractors, the two FP corrections, four new classes) — carried forward. - .websec-ignore: skip the maintainer's gitignored base-research/ on self-scan. 103 tests green on the integrated tree; wheel builds + installs + smoke-runs (reports 0.4.1, calibration + bundled rules ship); self-scan dogfoods clean (1 Dockerfile LOW, 36 suppressed). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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
Audit of the recon engine surfaced one critical silent false-negative plus a set of completeness/robustness gaps. All fixes are test- or run-verified — 80 → 88 tests, byte-compile clean, full pipeline + reproductions verified.
🔴 Critical
base.path_in_skip_dir).routes._in_skip_dir/scanners._in_skip_dirsplit the absolute noir/scanner path and checked every segment, so a repo living under a skip-named ancestor (.claude/worktrees/<id>,vendor/,target/,build/,out/,~/.cache,security/) had its ancestor segment match and every route + finding silently dropped — the tool reported the app as empty / "nothing wrong". Recurrence of bug-005 in two post-filters the original fix missed. Proven: identical fixture → 2 routes at a clean path, 0 under atarget/ancestor; now 2 at both. Fails open when a path can't be made relative (a silent drop is the dangerous direction for a security tool).🟠 High — completeness on large / scan-heavy repos
all), not the briefing's top-15 slice → no HIGH/CRITICAL CVE/secret dropped at rank #16+ from the ledger + calibration.files_truncatedin FACTS + CLI warning + briefing banner) instead of silently analyzing an arbitrary subset.endpoint_guardscap 400 → 5000 with disclosure (endpoint_guards_truncated) so the missing-auth ledger isn't silently capped on big monorepos.🟡 Medium
webhook-forgeryclass, CWE-345) — previously surfaced only in the briefing, never ranked/calibrated.integrations.SIG_VERIFYno longer matches the bare wordsignature(a comment like "no signature verification" suppressed the finding). Found during validation.endpoints_over_capin their summaries.🟢 Low / robustness / polish
cross_tenant_bolacoercesstr(tenant)→ no crash on numeric tenant ids.findings._sqlcountssql-orm/prisma(sql)labels → SQL-ORM+Mongo apps not mis-down-ranked.main | sh).__init__.pydocstring lists the real 7 run artifacts.Tests
+8 regressions: skip-dir relative-to-root, normalize keeps findings under a skip-named ancestor, full static set into ledger, webhook→ledger, SIG_VERIFY tightening, numeric tenant id, SQL-ORM datastore vocab. 88 tests green.
The Dockerfile Trivy pin follows the documented installer interface but was not
docker build-verified in this environment — smoke-test before relying on it.🤖 Generated with Claude Code