mirror of
https://github.com/usestrix/strix.git
synced 2026-11-01 07:02:32 +00:00
feat(report): reject a cited CVE when the package is known but the version is clean
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)
This commit is contained in:
parent
8af0d81418
commit
d963bbd9ef
2 changed files with 76 additions and 27 deletions
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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():
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue