From d963bbd9ef3c581d0c1692cab46ddf0448f47e6b Mon Sep 17 00:00:00 2001 From: Sean Turner Date: Mon, 3 Aug 2026 09:42:00 +0100 Subject: [PATCH] feat(report): reject a cited CVE when the package is known but the version is clean MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Refine the empty-result handling: when the provider returns no advisories for the installed version, distinguish two cases via a new AdvisoryProvider.knows_package (version-less query): - provider KNOWS the package (advisories on other versions) but none affect this version -> CONFIDENT out-of-range -> reject (e.g. minimist 1.2.6 cited for CVE-2021-44906, which was fixed in 1.2.6 — OSV knows minimist, 0 affect 1.2.6). - provider doesn't know the package at all -> genuine coverage gap (private / vendored / provider lag) -> still fail-open, emit. Without this the most common dependency false positive — a real CVE cited against a version that's already patched past its range — slipped through as a 'coverage gap'. Surfaced by a live scan: a weak model, coerced into over-claiming patched minimist CVEs, had them emitted; now they're correctly rejected while the genuinely in-range lodash CVEs still emit. Also refactor OsvProvider to a shared _query helper (used by both affecting and knows_package). 18 tests. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --- strix/report/dep_verify.py | 68 +++++++++++++++++++++++++++----------- tests/test_dep_verify.py | 35 ++++++++++++++++---- 2 files changed, 76 insertions(+), 27 deletions(-) diff --git a/strix/report/dep_verify.py b/strix/report/dep_verify.py index 8d5b8b504..ae7ce8652 100644 --- a/strix/report/dep_verify.py +++ b/strix/report/dep_verify.py @@ -79,6 +79,13 @@ class AdvisoryProvider(Protocol): def affecting(self, pkg: str, ecosystem: str, version: str) -> set[str] | None: ... + def knows_package(self, pkg: str, ecosystem: str) -> bool | None: + """Does the provider have ANY advisory for this package (any version)? + Lets the caller tell a CONFIDENT out-of-range (provider knows the package + but nothing affects the installed version) from a COVERAGE GAP (provider + doesn't know the package at all → fail open). None = can't determine.""" + ... + class OsvProvider: """AdvisoryProvider backed by an OSV-schema /v1/query endpoint. Default @@ -89,28 +96,38 @@ class OsvProvider: def __init__(self, url: str) -> None: self.url = url - def affecting(self, pkg: str, ecosystem: str, version: str) -> set[str] | None: + def _query(self, payload: dict[str, Any]) -> list[dict] | None: try: ca = os.environ.get("REQUESTS_CA_BUNDLE") - resp = requests.post( - self.url, timeout=20, - json={"package": {"name": pkg, "ecosystem": ecosystem}, "version": version}, - verify=ca if ca else True, - ) + resp = requests.post(self.url, timeout=20, json=payload, + verify=ca if ca else True) if resp.status_code != 200: - logger.info("dep-verify: OSV %s for %s@%s; can't answer", - resp.status_code, pkg, version) + logger.info("dep-verify: OSV %s for %s; can't answer", + resp.status_code, payload.get("package")) return None - vulns = resp.json().get("vulns", []) or [] + return resp.json().get("vulns", []) or [] except Exception: # noqa: BLE001 — advisory; a hiccup must never suppress a finding - logger.info("dep-verify: OSV query failed for %s@%s; can't answer", - pkg, version, exc_info=True) + logger.info("dep-verify: OSV query failed for %s; can't answer", + payload.get("package"), exc_info=True) + return None + + def affecting(self, pkg: str, ecosystem: str, version: str) -> set[str] | None: + vulns = self._query( + {"package": {"name": pkg, "ecosystem": ecosystem}, "version": version}) + if vulns is None: return None affecting: set[str] = set() for v in vulns: affecting |= _ids_for(v) return affecting + def knows_package(self, pkg: str, ecosystem: str) -> bool | None: + # version-less query returns every advisory for the package (any version). + vulns = self._query({"package": {"name": pkg, "ecosystem": ecosystem}}) + if vulns is None: + return None + return len(vulns) > 0 + def _resolve_provider(dep_settings: Any) -> AdvisoryProvider | None: """Build the AdvisoryProvider from DepVerifySettings. None => disabled/unknown @@ -161,14 +178,25 @@ def verify_dependency( # noqa: PLR0911 — the returns are deliberate fail-open ident, pkg, version) return None # confirmed: installed version IS in the CVE's range → emit (real) if not affecting: - # definitive "no advisories affect this version" is still a coverage-gap - # risk (private/vendored pkg, provider lag) → fail open, emit. - logger.info("dep-verify: provider lists NO advisories for %s@%s; emitting " - "(coverage gap, not a confident FP)", pkg, version) - return None + # Nothing affects the installed version. Disambiguate: + # - provider KNOWS the package (has advisories for OTHER versions) but + # none hit this version → CONFIDENT out-of-range → reject. + # - provider doesn't know the package at all (private/vendored, lag) → + # genuine coverage gap → fail open, emit. + knows = None + try: + knows = provider.knows_package(pkg, eco) + except Exception: # noqa: BLE001 — advisory + knows = None + if not knows: # False (unknown pkg) or None (can't tell) → fail open + logger.info("dep-verify: provider has no advisories for %s at all; " + "emitting (coverage gap, not a confident FP)", pkg) + return None + # else: package is known, this version is clean → fall through to reject. - # Provider knows advisories for this version but NOT the cited one → the - # installed version is out of the cited CVE's range → false positive. + # Provider knows advisories for this version but NOT the cited one (or knows + # the package and NOTHING affects this version) → the installed version is out + # of the cited CVE's range → false positive. return { "success": False, "error": ( @@ -176,8 +204,8 @@ def verify_dependency( # noqa: PLR0911 — the returns are deliberate fail-open f"not affect {pkg}@{version} per the advisory provider (installed " f"version is outside the advisory's vulnerable range — likely already " f"patched or the CVE is mis-attributed to this package). Advisories " - f"affecting {pkg}@{version}: {sorted(affecting)}. If you believe the " - f"version IS vulnerable, cite the exact affected range." + f"affecting {pkg}@{version}: {sorted(affecting) or 'none'}. If you " + f"believe the version IS vulnerable, cite the exact affected range." ), "verify_rejected": True, "dep_version_out_of_range": True, diff --git a/tests/test_dep_verify.py b/tests/test_dep_verify.py index fd875fcc4..8cebfe98e 100644 --- a/tests/test_dep_verify.py +++ b/tests/test_dep_verify.py @@ -25,16 +25,21 @@ def _cand(**kw): class _FakeProvider: - """Returns a fixed affecting-set (or None for can't-answer).""" - def __init__(self, affecting): + """Returns a fixed affecting-set (None for can't-answer). `knows` is what + knows_package returns (True=package known, False=unknown, None=can't tell).""" + def __init__(self, affecting, knows=None): self._a = affecting + self._knows = knows def affecting(self, _pkg, _ecosystem, _version): return self._a + def knows_package(self, _pkg, _ecosystem): + return self._knows -def _prov(affecting): - return _FakeProvider(affecting) + +def _prov(affecting, knows=None): + return _FakeProvider(affecting, knows) # --- verdict logic (explicit provider, no network) --- @@ -53,9 +58,25 @@ def test_out_of_range_rejects(): assert "CVE-2021-23337" in out["error"] -def test_empty_set_fails_open(): - # definitive "no advisories affect this version" -> coverage-gap risk -> emit. - assert dv.verify_dependency(_cand(), _prov(set())) is None +def test_empty_set_unknown_package_fails_open(): + # empty affecting + provider doesn't know the package (knows=False) -> + # genuine coverage gap (private/vendored) -> emit. + assert dv.verify_dependency(_cand(), _prov(set(), knows=False)) is None + + +def test_empty_set_cant_tell_fails_open(): + # empty affecting + knows_package can't answer (None) -> fail open, emit. + assert dv.verify_dependency(_cand(), _prov(set(), knows=None)) is None + + +def test_empty_set_known_package_rejects(): + # empty affecting BUT provider KNOWS the package (advisories on other versions) + # -> confident out-of-range -> REJECT. The minimist@1.2.6 case a live scan of a + # weak model exposed: OSV knows minimist but 0 advisories affect 1.2.6. + out = dv.verify_dependency(_cand(), _prov(set(), knows=True)) + assert out is not None + assert out["verify_rejected"] is True + assert out["dep_version_out_of_range"] is True def test_provider_cant_answer_fails_open():