fix(scan): require explicit trust for .wardline/judged.yaml suppressions#24
fix(scan): require explicit trust for .wardline/judged.yaml suppressions#24tachyon-beep wants to merge 1 commit into
Conversation
…ail-on) (#28) Close a HIGH-severity CI-gate bypass. `wardline scan --fail-on` applied repository-controlled suppressions (`.wardline/baseline.yaml`, `wardline.yaml` waivers, `.wardline/judged.yaml`) to findings BEFORE evaluating the gate, so a malicious PR could commit a suppression keyed to its own new defect's fingerprint and clear the gate. All three sources are committed repo content and equally exploitable. Reproduced live (baselining the sole ERROR zeroed the gate). Secure-by-default model (combines #24 + #25): - `gate_decision` now evaluates a separate UNSUPPRESSED population (`ScanResult.gate_findings`). baseline/waiver/judged still ANNOTATE the emitted findings (`suppressed=…` stays visible) but no longer clear the gate. - The gate population is built with apply_suppressions over EMPTY baseline + waivers + judged, NOT `list(raw)`, so the lineless-DEFECT→non-gating-FACT downgrade is preserved (no spurious gate trips). - `--new-since <ref>` (operator-supplied, unforgeable) scopes BOTH the emitted and gate populations — the secure CI ratchet. - `--trust-suppressions` (CLI) / `trust_suppressions` (run_scan, MCP scan tool), default False, restores the local ratchet / judge DX for trusted checkouts (None sentinel → gate falls back to the suppressed findings). `run_judge` passes True so judge/triage/persist are unchanged. - `load_judged` now requires `verdict: FALSE_POSITIVE` (rejects a hand-edited TRUE_POSITIVE / missing verdict smuggled in as a silent suppression). BREAKING (noted in CHANGELOG, acceptable at 0.x): baseline-gated CI goes green→red on upgrade until `--new-since` or `--trust-suppressions` is added. Docs updated (suppression.md): the secure CI ratchet is `--new-since`. Combines and supersedes #24 (judged-only) and #25 (no escape hatch + a lineless-DEFECT gate bug). Full suite green (2394 passed), ruff + mypy clean. Co-authored-by: John Morrissey <john@wardline.dev> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Fixed ourselves in The vulnerability is real: a repo-committed #28 closes all three vectors: by default the gate evaluates the unsuppressed population; baseline/waiver/judged still annotate findings but don't clear the gate. The secure CI ratchet is the operator-supplied Your contributions were kept, not discarded:
Full suite 2406 passed, ruff + mypy clean, repro confirms the gate trips on a suppressed defect by default. Thanks — closing in favour of #28. |
Motivation
.wardline/judged.yamlwas being applied byscanbefore gating, allowing untrusted judged records to suppress real defects and clear--fail-onin CI.Description
run_scan()ignoreload_judged(root / ".wardline" / "judged.yaml")unless the caller setstrust_judged_suppressions=True(defaultFalse), so judged suppressions are opt-in for trusted checkouts via an explicit operator decision.--trust-judged-suppressionstowardline scanand thread it through the initial scan and the post-autofix rescan so local/trusted workflows are preserved when requested.run_scan(..., trust_judged_suppressions=True)fromrun_judge()sojudge --writeand triage flows still consult/write judged records as expected.load_judged()to require averdictfield and reject any record whereverdict != "FALSE_POSITIVE"to avoid accepting forged/non-FP entries as suppressions.Testing
PYTHONPATH=src python -m py_compile ...on the modified modules, which succeeded.uv run ruff check ..., which passed.pytestruns (e.g.uv run --extra scanner pytest tests/unit/core/test_judged.py tests/unit/cli/test_cli.py::test_judge_write_then_scan_gate_requires_trust_flag tests/unit/cli/test_cli.py::test_scan_with_fix_rescan_preserves_strict_defaults -q) but the test run was blocked by environment/network limitations while fetching extras (jsonschema) and missing runtime packages (yaml), so full automated tests could not be completed in this environment.Codex Task