fix(cli): report the findings that actually drove the risk score - #375
fix(cli): report the findings that actually drove the risk score#375wernerkasselman-au wants to merge 3 commits into
Conversation
Four call sites selected findings with `filtered_findings or findings`, which over-reports in two distinct ways. The falsy fallback. `report` returns `filtered_findings` as a real list, and an empty one is a real answer: every finding was filtered out by the meta-analyzer, or suppressed by a baseline. The `or` treats `[]` as absent and falls through to the raw pre-filter `findings`, so a skill that scores 0 is reported with a non-zero finding count. The unsubtracted partition. `report` returns `filtered_findings` as the full pre-partition set (kept plus baseline-suppressed) alongside `suppressed_findings`, and scores, dedupes, and builds SARIF from the kept subset alone. Counting `filtered_findings` therefore counts findings the report itself excluded. Adds `suppression.effective_findings()`, the inverse of the existing `partition_findings()`, and routes all four sites through it: - the recursive multi-skill summary table (`cli.py`) - the combined recursive JSON report (`cli.py`) - `skillspector baseline`, which previously fingerprinted raw findings the scan had already filtered out - the MCP `scan_skill` verdict, which serialises this list straight to a calling agent, so a suppressed finding leaking in tells that agent a skill is dirtier than the score it is gating on It falls back to the raw `findings` list only when `filtered_findings` is absent or malformed, and does not subtract there, since raw findings are not the population that produced `suppressed_findings`. Verified by reverting the source change against the new tests: all five behavioural tests fail on the current code at exactly the site each one targets, and pass with the fix. 2088 passed, 17 skipped, 4 xfailed. Signed-off-by: Werner Kasselman <145896621+wernerkasselman-au@users.noreply.github.com>
A mutation harness against the shipped suite ran nineteen mutants: thirteen killed, six survivors. A survivor is an unprotected behaviour even when the code is correct, and two of these were real coverage holes rather than defensive noise. The two that mattered: - The `skillspector baseline` call site had no site-level test at all. Reverting `cli.py` to the old `filtered_findings or findings` left the entire suite green, so the changed fingerprinting behaviour was completely unprotected. That line was flagged twice. - `effective_findings` subtracts by `finding_id`, but swapping both comparisons to `rule_id` also left the suite green, because no test had a kept and a suppressed finding sharing a rule id. Two hits of one rule at different sites is the common case, and keying on `rule_id` would drop the finding that was never baselined. The remaining four covered the malformed-result guards: the suppressed container type check, the `SuppressedFinding` entry check, the `entry.finding is not None` check, and the filtered-item `Finding` check. The container check needed a non-iterable value to be observable, since a truthy non-list string iterates harmlessly and yields the same answer; an int or float raises TypeError out of the comprehension without the guard. Seven new tests, no source change. Each was verified to fail against its own mutant and pass against the restored source, so none of them is green by accident. 2098 passed, 17 skipped, 4 xfailed. Ruff clean. Mypy unchanged at 117 errors in 21 files, the same count as base. Signed-off-by: Werner Kasselman <145896621+wernerkasselman-au@users.noreply.github.com>
Brings the branch up to `5680c2c`. Both earlier conflicts were purely additive and both sides were kept: `cli.py` (main's NVIDIA#286 `discover_baseline` alongside this branch's `effective_findings` in the same sorted import block) and `tests/unit/test_suppression.py` (main's `discover_baseline` tests alongside this branch's `effective_findings` tests). Checked that NVIDIA#286 does not undermine this branch. `discover_baseline` is called only from `scan()` behind `--use-shipped-baseline`; the `baseline` generation command still calls `_scan_state()` without a baseline, so `state["baseline"]` is never set there and its `suppressed_findings` stays empty. The opt-in shipped-baseline path widens where suppression is active, which is more surface for this fix, not less. All four call sites remain routed through `effective_findings`, and no falsy `filtered_findings or findings` fallback was reintroduced. Merged tree: 2210 passed, 17 skipped, 4 xfailed. Ruff clean across 175 files. Signed-off-by: Werner Kasselman <145896621+wernerkasselman-au@users.noreply.github.com>
c250598 to
17bf741
Compare
|
Hi Keshav, You asked me to address review comments and resolve merge conflicts on #205 and #375 and #377 are the two that are ready. Both are rebased on current
#19 I closed myself. It was based on #205 is the one I would like a decision on rather than a rebase. I can Two things I should be straight about on my own verification. I ran the full One housekeeping note on #375: I force-pushed it once after the merge, to tidy Happy to take any of this differently, and if you would rather I fold #377 into Thanks, |
|
Correcting one thing I said above, in the direction of more confidence rather I said Docker was not installed here, which was true, but I had only checked
The only difference from CI is the SELinux flag and the runtime being podman Werner |
What this fixes
Four call sites select findings with
filtered_findings or findings. Thatidiom over-reports in two distinct ways, and both are reachable on
maintoday.
1. The falsy fallback.
reportreturnsfiltered_findingsas a reallist, and an empty one is a real answer: the meta-analyzer filtered every
finding out, or a baseline suppressed them all. The
ortreats[]asabsent and falls through to the raw pre-filter
findings, so a skill thatscores 0 is reported alongside a non-zero finding count, disagreeing with
its own risk score and report body.
2. The unsubtracted partition.
reportreturnsfiltered_findingsasthe full pre-partition set and
suppressed_findingsas the excluded subset:Score, dedup, and SARIF all come from
active_findings. Countingfiltered_findingscounts findings the report itself excluded.The change
Adds
suppression.effective_findings(), the inverse of the existingpartition_findings()and placed next to it, and routes all four sitesthrough it:
cli.pyrecursive summary tablecli.pycombined recursive JSONfinding_countdisagreed withrisk_scorecli.pyskillspector baselinemcp_server.pyscan_skillverdictIt falls back to the raw
findingslist only whenfiltered_findingsisabsent or malformed, and deliberately does not subtract there: raw findings
are not the population that produced
suppressed_findings.Tests
15 new tests. 10 unit-cover every branch of
effective_findings(emptyfiltered, partial and full suppression, no-baseline pass-through, raw
fallback without subtraction, malformed input, empty result). 5 pin the
behaviour at the call sites: the recursive summary row, the combined JSON
count, and the two MCP verdict paths. The summary row and the JSON count
are pinned separately on purpose, since they are independent lines and a
regression in one is invisible to a test covering the other.
I verified these tests actually catch the bug rather than passing by
accident: reverting only the
src/change and re-running leaves all fivebehavioural tests failing, each at the site it targets, and they pass again
with the fix restored.
mypyreports the same 117 pre-existing errors in 21 files before andafter this change, so nothing new is introduced there.
Scope
Deliberately narrow, and independent of the open discussion on #201 and
#205 about whether
--baselineshould be supported in recursive mode atall. This corrects how findings are counted and reported wherever the
partition already exists; it does not change what gets suppressed. The
review on #205 flagged this same falsy pattern as worth fixing in a
follow-up, which is what this is.