From 675608dc7ad233f6d707e31aca519b8b28e56f3c Mon Sep 17 00:00:00 2001 From: yoni Date: Mon, 9 Nov 2026 00:45:23 +0000 Subject: [PATCH] reporting: one reachability verdict per dependency CVE Replace the five-value usage ladder (not_imported / imported / vulnerable_symbol_used / reachable_call_path / unknown) with three verdicts that answer whether the advisory's vulnerable code runs in this codebase: reachable, not_reachable, not_applicable. The verdict is required, per CVE, with evidence; there is no unknown. The skill walks every introducing chain of a transitive dependency, checks the affected symbol per advisory, and traces the call to its source before deciding. --- .../skills/custom/dependency_cve_scanning.md | 191 +++++++++--------- strix/tools/reporting/tool.py | 101 +++++---- tests/test_reporting_fields.py | 41 ++-- 3 files changed, 167 insertions(+), 166 deletions(-) diff --git a/strix/skills/custom/dependency_cve_scanning.md b/strix/skills/custom/dependency_cve_scanning.md index 349dd2b79..5e8316272 100644 --- a/strix/skills/custom/dependency_cve_scanning.md +++ b/strix/skills/custom/dependency_cve_scanning.md @@ -114,99 +114,103 @@ not just "upgrade to ". ### Usage / reachability analysis (required for every dependency CVE) -For every CVE you are about to report, run a static usage analysis and record -the result in the structured `reachability` + `reachability_evidence` fields, -and report every usage site you find as a `code_locations` entry (see Reporting). -The level is an **evidence ladder, never an exploitability verdict** — claim -only what you proved, and cite the proof. It never changes severity (that is -`advisory_cvss` alone); it exists so the reader can prioritize. +Every CVE you report carries a verdict in `reachability`, the proof in +`reachability_evidence`, and every usage site in `code_locations` (see +Reporting). The verdict answers one question for THIS advisory in THIS +codebase: does the vulnerable code run here? There are three answers and no +"unknown". The tool rejects a report without a verdict or without evidence, +so finish the analysis before you file. -**Go — use govulncheck (real call-graph analysis):** +- **`reachable`** — the advisory's vulnerable code executes in this codebase: + application code, or a package it calls on the application's behalf, calls + the affected function, mode or feature. Evidence: the call site as + `file:line` and the source-to-sink trace (step 3). +- **`not_reachable`** — the package is loaded by the shipped code, but this + CVE's vulnerable path does not execute here. Evidence: what you checked and + why the path is dead. Typical reasons: no caller of the affected API after + a complete search; the only call site is behind a condition that is false + here (a polyfill for an older runtime, a flag that is off, a dev-only + command); the parent package of a transitive dependency never references + the affected API; the flaw sits in a mode or option this code never uses; + the argument that triggers the flaw is a constant here. +- **`not_applicable`** — the advisory does not apply to this codebase at all: + nothing the shipped code loads imports the package; it is build, test or + dev tooling the artifact never executes; the installed copy is a patched or + unaffected build; the flaw is specific to a platform this code does not run + on; or the only code that could use it cannot be inspected (a compiled + blob, a private bundle with no source) and nothing you can inspect reaches + it. Evidence: the search scope and the negative result. -```bash -# Symbol-level: reports only vulnerabilities whose vulnerable functions are -# actually reachable from application code. Needs the Go toolchain + module -# deps; if either is missing, fall back to the checks below rather than -# claiming a level. -if command -v govulncheck >/dev/null && go version >/dev/null 2>&1; then - govulncheck -format json ./... > "$ART/govulncheck.json" || true -fi -``` +The verdict is per CVE, not per package. Sibling CVEs on one package usually +affect different symbols or modes (one hits a parser, another a header +check): run the check against each advisory's own affected functions and +never copy one CVE's verdict or evidence onto another. The import check +(step 1) is the only step shared across a package's CVEs. -- A finding with a call stack ⇒ `reachability=reachable_call_path`, put the - call-path excerpt (entrypoint → vulnerable function) in - `reachability_evidence`. -- Listed as affecting a required module but with no reachable symbol ⇒ fall - back to the import/symbol checks below (`imported` / `not_imported`). +`reachable` is not an exploit, and `not_reachable` / `not_applicable` are +not proof of safety (dynamic `import()`, reflection and framework wiring +evade static search). Say what you checked; never write "not exploitable". -**All other ecosystems — import check, then symbol match:** +**Procedure** 1. **Import check.** Search application code (exclude lockfiles, vendored - deps, `node_modules`, build output) for imports of the vulnerable package: - `ast-grep`/`rg` for `import`/`require`/`from X import` of the package (and - its ecosystem import name, which may differ from the registry name, e.g. - `PyYAML` → `yaml`). No hits ⇒ `not_imported`, with the search scope stated - in `reachability_evidence`. For a **transitive** dependency, the check is - whether application code imports it directly; if not, it is reachable only - through the direct dependency — check whether the direct dep's usage can - hit it (if unclear, use `imported` when the direct dep is used at all). -2. **Symbol match — per CVE, not per package.** Read each CVE's own advisory - (GHSA/NVD/OSV `affected[].ecosystem_specific.imports` or the advisory - text) for the affected functions/classes/APIs. Search application code for - those symbols (`ast-grep` pattern or `rg -n`). Hits ⇒ - `vulnerable_symbol_used`, with repo-relative `file:line` of each hit (up - to a handful) in `reachability_evidence`. Imported but no affected-symbol - usage found (or the advisory names no symbols) ⇒ `imported`. - Different CVEs on the same package usually affect **different** symbols - (one hits a parser, another a header check) — never copy one CVE's - verdict/evidence onto its siblings; run the symbol search against each - CVE's own affected-symbol list. The import check (step 1) is the only - part shared across a package's CVEs. -3. **Source-to-sink trace — do this whenever step 2 found a symbol hit.** A - symbol hit alone says the code calls the vulnerable API; it does not say - who can reach it. Start at the sink (the exact line that calls the - vulnerable function) and walk backwards hop by hop to the source: the - entry point that carries untrusted input (HTTP route, CLI argument, queue - or webhook payload, uploaded file, config value). Read each intermediate - function; when a hop is a thin wrapper, go one step deeper — never stop at - the first caller. Record what each hop enforces: authentication, a role - check, validation, a feature flag, a size or type limit, a default that is - off in production. - Write the chain into `reachability_evidence` as - `entry point -> intermediate call -> package call` with a - repository-relative `file:line` for every hop, and say who controls the - input. If no source reaches the sink, say that too — the level stays - `vulnerable_symbol_used` (the call is real), and the trace is what tells - the reader it is only reachable from, say, an operator CLI. -4. If the analysis was not performed or is inconclusive (obfuscated code, - dynamic loading, unparsable sources) ⇒ `unknown` and say why in - `assumptions`. + deps, `node_modules`, build output) for imports of the vulnerable package + under its ecosystem import name (`PyYAML` → `yaml`): `ast-grep`/`rg` for + `import`/`require`/`from X import`. For a **transitive** dependency nothing + in application code imports it, so walk every introducing chain from the + lockfile (e.g. `express > body-parser > qs`) and inspect the code of the + package one hop above the vulnerable one on each chain (`body-parser`, not + just `express`; its installed sources, or the class references of its + jar). Nothing imports it and no chain loads it ⇒ `not_applicable`, citing + the scope you searched. Loaded ⇒ continue. +2. **Affected-symbol check, per CVE.** Read the advisory (GHSA/NVD/OSV + `affected[].ecosystem_specific.imports`, the advisory text, the fix commit + when the text is vague) for the functions, classes, options or modes that + trigger the flaw. Search application code, and the parent package of a + transitive dependency, for them (`ast-grep` pattern or `rg -n`; sibling + CVEs can share one multi-pattern search per package). No caller after a + complete search ⇒ `not_reachable`, citing the symbols and the scope. A + caller ⇒ step 3. When the flaw is in code every use of the package runs + (the parser the package exists for), the import itself is the call site ⇒ + step 3. +3. **Source-to-sink trace.** Start at the sink (the exact line that calls the + affected function) and walk backwards hop by hop to the source: the entry + point that carries the input (HTTP route, CLI argument, queue or webhook + payload, uploaded file, config value). Read each intermediate function; + when a hop is a thin wrapper, go one step deeper — never stop at the first + caller. Read what reaches the function: a constant argument that cannot + contain the triggering pattern, a flag that is off, a runtime-version + gate, a path only a dev tool runs means the vulnerable path does not run + ⇒ `not_reachable`, citing the condition. Otherwise ⇒ `reachable`. Write + the chain into `reachability_evidence` as + `entry point -> intermediate call -> package call` with a repo-relative + `file:line` for every hop, what each hop enforces (authentication, a role + check, validation, a size or type limit), and who controls the input. A + trusted or operator-only source does not soften the verdict: the code + runs, so it is `reachable`, and the trace is what tells the reader who can + reach it; the contextual CVSS carries that. +4. **Go — govulncheck first.** When the Go toolchain and module deps are + present, `govulncheck -format json ./... > "$ART/govulncheck.json"` gives a + real call graph: a finding with a call stack ⇒ `reachable`, with the call + path (entrypoint → vulnerable function) as the trace; an affected module + with no reachable symbol ⇒ `not_reachable` when application code imports + it (cite the govulncheck result and the import), `not_applicable` when + nothing imports it. When govulncheck cannot analyze the module (build + errors, missing deps), run steps 1–3 instead of guessing. -Cheap-first budgeting: the import check is one search per package — always do -it. Do the per-CVE symbol match for every CVE whose advisory names affected -symbols (they can be batched into one multi-pattern search per package); -prioritize `critical`/`high`/KEV when the budget is tight; a CVE whose symbol -search was skipped may still be reported as `imported` (the import check is -real evidence), but its `reachability_evidence` must state that the -affected-symbol check was not performed, so a skipped search is never -mistaken for a completed one with no hits. Never let this analysis stall -reporting — `unknown` with a reason beats an unverified claim. +Work in severity order (critical, high and KEV first) so that if time runs +out the low tail is what is missing, and name that tail in the scan summary. +A CVE whose analysis you did not finish is neither filed with a guessed +verdict nor filed as `not_applicable`: finish it, or report the gap. Never let +the usage analysis silently drop a CVE: every advisory the scanner matched is +either filed with a verdict or named in the summary as not analyzed. -Anti-overclaim rules: +### Reachability is a verdict on the code path, not on severity -- `not_imported` still does NOT mean safe (dynamic `import()`/reflection/ - framework wiring evade static search) — never phrase it as "not exploitable". -- `reachable_call_path` is reserved for call-graph tools (govulncheck); a - symbol grep hit is `vulnerable_symbol_used`, no matter how convinced you are. -- The tool rejects any level other than `unknown` without - `reachability_evidence`. - -### Reachability is a confidence modifier, not a gate - -Do NOT suppress or downgrade a known CVE just because you could not prove the -vulnerable code path is reachable. Report it, set `advisory_cvss` from the -advisory, record the usage analysis in `reachability`/`reachability_evidence`, -and use `assumptions` for anything softer. If you *can* actually trigger the +Do NOT suppress or downgrade a known CVE because its vulnerable code path is +not reached. Report it with its verdict, set `advisory_cvss` from the +advisory, put what the analysis proved in `reachability_evidence`, and use +`assumptions` for anything softer. If you *can* actually trigger the vulnerable path or chain it into a dynamic exploit, additionally report that as a normal dynamic finding with `create_vulnerability_report` (the standalone CVE stays in its own `create_dependency_report`). @@ -230,8 +234,8 @@ findings and rejects empty PoC fields): pins the vulnerable version. Each entry needs the repo-relative `file`, the exact `start_line` / `end_line`, the verbatim `snippet` at those lines, and a short `label` (e.g. "imports js-yaml", "calls yaml.load on CLI input"). Read - the real file to confirm the lines; never guess. When the package is - `not_imported`, the manifest line is the only entry. If an entry carries + the real file to confirm the lines; never guess. When the verdict is + `not_applicable`, the manifest line is the only entry. If an entry carries `fix_before` / `fix_after`, also fill `fix_verification`. - **Always fill the structured dependency fields** (they power the dedicated dependency-report card; do not leave them only in free-text): @@ -260,9 +264,8 @@ findings and rejects empty PoC fields): - Do NOT cap severity at LOW just because there is no dynamic reproduction — use the advisory score. - Set `reachability` + `reachability_evidence` from the usage analysis above — - the tool rejects a report with no evidence, so for `unknown` write what you - searched and why the result is inconclusive; - use `assumptions` for anything softer (confidence, caveats, analysis limits). + the tool rejects a report without a verdict or without evidence; use + `assumptions` for anything softer (confidence, caveats, analysis limits). - **Always set `contextual_cvss_breakdown` + `contextual_cvss_reasoning`.** Every dependency finding carries a contextual rating of the CVE in this codebase (see below). Start from the published metrics and change only what your @@ -296,8 +299,8 @@ evidence proves is different in this codebase: deployment requires before the path is reachable. - `scope` `U`/`C` — whether exploitation here escapes the component boundary. - `confidentiality`/`integrity`/`availability` `N`/`L`/`H` — the impact in this - codebase. `not_imported` code the build still ships is usually `N` across all - three. + codebase. A `not_applicable` package the build still ships is usually `N` + across all three. Ground every metric in the **source-to-sink trace** from the usage analysis (step 3 above), not in a general impression of the package. Derive the metrics @@ -308,7 +311,7 @@ preconditions the hops enforce; `confidentiality`, `integrity`, and When you have no source-to-sink trace, still rate the finding: copy the published metrics, change only the metrics the usage level itself proves, and -say so in the reasoning. For example, for a `not_imported` package that the +say so in the reasoning. For example, for a `not_applicable` package that the build still ships, keep the published metrics and lower `confidentiality`, `integrity`, and `availability` to `N`, because no code path reaches the vulnerable symbol. Never invent a hop you did not read. @@ -344,8 +347,8 @@ hallucinate a CVE id. - Do not silently drop a known CVE because it lacks a dynamic PoC — that is the exact failure this skill prevents. - Do not downgrade advisory severity for lack of dynamic reproduction. -- Do not claim a `reachability` level the evidence does not prove — `unknown` - with a reason is always acceptable; an overclaimed level never is. +- Do not claim a `reachability` verdict the evidence does not prove, and do + not file a CVE whose analysis you did not finish — report the gap instead. - Do not send a report without `contextual_cvss_breakdown` and `contextual_cvss_reasoning` — the reader rates and ranks the finding with them. - Do not use the contextual breakdown to quietly de-rate a CVE you could not diff --git a/strix/tools/reporting/tool.py b/strix/tools/reporting/tool.py index 3faf7fbd6..378687764 100644 --- a/strix/tools/reporting/tool.py +++ b/strix/tools/reporting/tool.py @@ -1689,15 +1689,7 @@ def _dependency_severity(advisory_cvss: float | None) -> tuple[float, str]: return score, "none" -_VALID_REACHABILITY = frozenset( - { - "not_imported", - "imported", - "vulnerable_symbol_used", - "reachable_call_path", - "unknown", - } -) +_VALID_REACHABILITY = frozenset({"reachable", "not_reachable", "not_applicable"}) def _validate_manifest_path(manifest_path: str | None) -> str | None: @@ -1838,11 +1830,12 @@ def _build_dependency_metadata( _REACHABILITY_EVIDENCE_LABELS = { - "not_imported": "not imported by application code", - "imported": "imported by application code; affected API usage unconfirmed", - "vulnerable_symbol_used": "the advisory's affected API is used in application code", - "reachable_call_path": ( - "a call path from application code to the vulnerable function was proven" + "reachable": "the advisory's vulnerable code runs in this codebase", + "not_reachable": ( + "the package is loaded, but this advisory's vulnerable code does not run here" + ), + "not_applicable": ( + "the advisory does not apply here: the shipped code never loads the vulnerable package" ), } @@ -1899,7 +1892,7 @@ async def _do_create_dependency( # noqa: PLR0912, PLR0915 introduced_by: str | None = None, dependency_path: str | None = None, manifest_path: str | None = None, - reachability: str = "unknown", + reachability: str | None = None, reachability_evidence: str | None = None, code_locations: list[dict[str, Any]] | None = None, fix_verification: str | None = None, @@ -1955,17 +1948,22 @@ async def _do_create_dependency( # noqa: PLR0912, PLR0915 ) errors.extend(_validate_fix_verification(parsed_locations, fix_verification)) - reachability = (reachability or "unknown").strip().lower() - if reachability not in _VALID_REACHABILITY: + reachability = (reachability or "").strip().lower() + if not reachability: + errors.append( + "reachability is required: 'reachable', 'not_reachable' or 'not_applicable'. " + "There is no 'unknown': finish the usage analysis for this CVE before filing it." + ) + elif reachability not in _VALID_REACHABILITY: errors.append( f"Invalid reachability: {reachability!r}. Must be one of: {sorted(_VALID_REACHABILITY)}" ) elif not (reachability_evidence or "").strip(): errors.append( - "reachability_evidence is required: cite the concrete proof (import " - "file:line, matched symbol usage, or govulncheck call path), or, for " - "'unknown', say what you searched and why the result is inconclusive. " - "Never claim a reachability level without evidence." + "reachability_evidence is required: cite the concrete proof (the call site " + "and its source-to-sink trace for 'reachable'; the complete search or the " + "condition that keeps the vulnerable path from running for 'not_reachable'; " + "the search scope for 'not_applicable'). Never claim a verdict without evidence." ) errors.extend(_validate_contextual_cvss(contextual_cvss_breakdown, contextual_cvss_reasoning)) @@ -2115,7 +2113,7 @@ async def create_dependency_report( fix_effort: str = "low", introduced_by: str | None = None, dependency_path: str | None = None, - reachability: str = "unknown", + reachability: str | None = None, reachability_evidence: str | None = None, code_locations: list[dict[str, Any]] | None = None, fix_verification: str | None = None, @@ -2145,23 +2143,26 @@ async def create_dependency_report( - Re-reporting the same CVE/package already filed. **Reachability**: do NOT silently downgrade or suppress a finding - because the vulnerable code path may be unreachable — report it, and - record what the usage analysis showed via the structured - ``reachability`` + ``reachability_evidence`` fields (see the - dependency-cve-scanning skill for the analysis procedure). The level - is an evidence ladder, never an exploitability verdict: + because the vulnerable code path is not reached — report it, and record + the verdict of the usage analysis in the structured ``reachability`` + + ``reachability_evidence`` fields (see the dependency-cve-scanning skill + for the procedure). The verdict answers one question for this advisory + in this codebase, "does the vulnerable code run here?", and the tool + rejects a report without it or without evidence: - - ``not_imported`` — the package is never imported/required by - application code (strongest de-prioritization signal; still not - proof of safety — dynamic loading, reflection, or framework wiring - can evade static search). - - ``imported`` — application code imports the package, but usage of - the advisory's affected API was not confirmed. - - ``vulnerable_symbol_used`` — the advisory's affected - function/class/API appears in application code. - - ``reachable_call_path`` — a call-graph tool (e.g. ``govulncheck``) - proved a path from application code to the vulnerable function. - - ``unknown`` — usage analysis was not performed or was inconclusive. + - ``reachable`` — the advisory's vulnerable code executes here: + application code, or a package it calls on the application's behalf, + calls the affected function, mode or feature. + - ``not_reachable`` — the package is loaded, but this advisory's + vulnerable path does not execute here (no caller of the affected API + after a complete search, a call behind a condition that is false here, + a parent package that never references the affected API, a mode this + code never uses, a triggering argument that is a constant here). + - ``not_applicable`` — the advisory does not apply to this codebase: + nothing the shipped code loads imports the package, it is build or + test tooling the artifact never runs, the installed copy is a patched + or unaffected build, or the flaw is specific to a platform this code + does not run on. Severity comes from ``contextual_cvss_breakdown`` when you provide one (computed exactly like a normal finding's ``cvss_breakdown``), otherwise @@ -2209,26 +2210,24 @@ async def create_dependency_report( checkout directory prefix so the path is relative to the repository root. This binds the finding to its exact file so remediation can target the right repository. - reachability: Usage-evidence level from static analysis — one of - ``not_imported`` / ``imported`` / ``vulnerable_symbol_used`` / - ``reachable_call_path`` / ``unknown``. Claim only what the - evidence proves; when in doubt use ``unknown``. + reachability: **Required.** The verdict of the usage analysis for + this CVE — ``reachable`` / ``not_reachable`` / ``not_applicable``. + Claim only what the evidence proves; there is no ``unknown``, + finish the analysis before filing. reachability_evidence: **Required.** The concrete proof for the - claimed level, or, for ``unknown``, what you searched and why - the result is inconclusive: repo-relative - ``file:line`` of the import or symbol usage, the matched - advisory symbols, or the govulncheck call-path excerpt. - Whenever you found the vulnerable symbol in use, also give the + verdict: repo-relative ``file:line`` of the import and of the + call that reaches the affected symbol, the search scope and + negative result behind ``not_applicable``, or the condition that + keeps the vulnerable path from running behind ``not_reachable``. + Whenever the affected symbol is called, also give the **source-to-sink trace** here: start at the vulnerable package call site and walk backwards hop by hop to the entry point - that carries untrusted input (HTTP route, CLI argument, queue + that carries the input (HTTP route, CLI argument, queue message, webhook, config file), going one step deeper whenever a hop is a wrapper. Write it as ``entry point -> intermediate call -> package call`` with a ``file:line`` per hop, name what each hop enforces (auth, role check, validation, a flag that - is off in production), and say who controls the input. State - it plainly when no entry point reaches the sink — that is the - most useful result a reader can get. + is off in production), and say who controls the input. code_locations: Where application code imports or calls the package — the evidence behind your ``reachability`` claim. List of dicts, same shape as ``create_vulnerability_report``:: diff --git a/tests/test_reporting_fields.py b/tests/test_reporting_fields.py index c895847c6..dfa4c4dc5 100644 --- a/tests/test_reporting_fields.py +++ b/tests/test_reporting_fields.py @@ -336,7 +336,7 @@ async def test_dependency_report_sets_class_and_metadata(report_state: ReportSta advisory_cvss=7.2, technical_analysis=None, fix_effort="trivial", - reachability="imported", + reachability="not_reachable", reachability_evidence=_DEP_EVIDENCE, contextual_cvss_breakdown=_DEP_CONTEXT, contextual_cvss_reasoning=_DEP_REASONING, @@ -357,7 +357,7 @@ async def test_dependency_report_sets_class_and_metadata(report_state: ReportSta "package_ecosystem": "npm", "manifest_path": "package-lock.json", "fixed_version": "4.17.21", - "reachability": "imported", + "reachability": "not_reachable", "reachability_evidence": _DEP_EVIDENCE, "contextual_cvss_breakdown": _DEP_CONTEXT, "contextual_cvss_score": pytest.approx(7.5, abs=0.05), @@ -386,7 +386,7 @@ async def test_dependency_report_records_transitive_chain(report_state: ReportSt fix_effort="trivial", introduced_by="express@4.18.1", dependency_path="express@4.18.1 > body-parser@1.20.0 > qs@6.10.2", - reachability="imported", + reachability="not_reachable", reachability_evidence=_DEP_EVIDENCE, contextual_cvss_breakdown=_DEP_CONTEXT, contextual_cvss_reasoning=_DEP_REASONING, @@ -427,7 +427,7 @@ async def test_dependency_report_persists_code_locations(report_state: ReportSta advisory_cvss=7.2, technical_analysis=None, fix_effort="trivial", - reachability="vulnerable_symbol_used", + reachability="reachable", reachability_evidence=_DEP_EVIDENCE, code_locations=[ { @@ -468,7 +468,7 @@ async def test_dependency_report_fix_locations_require_verification( advisory_cvss=7.2, technical_analysis=None, fix_effort="trivial", - reachability="imported", + reachability="not_reachable", reachability_evidence=_DEP_EVIDENCE, code_locations=[ { @@ -509,7 +509,7 @@ async def test_dependency_report_fix_locations_persist_with_verification( advisory_cvss=7.2, technical_analysis=None, fix_effort="trivial", - reachability="imported", + reachability="not_reachable", reachability_evidence=_DEP_EVIDENCE, code_locations=[ { @@ -556,7 +556,7 @@ async def test_dependency_report_rejects_unusable_locations( advisory_cvss=7.2, technical_analysis=None, fix_effort="trivial", - reachability="imported", + reachability="not_reachable", reachability_evidence=_DEP_EVIDENCE, code_locations=[{"file": "/abs/path.ts", "snippet": "require('lodash')"}], contextual_cvss_breakdown=_DEP_CONTEXT, @@ -587,7 +587,7 @@ async def test_dependency_report_omits_blank_chain_fields(report_state: ReportSt fix_effort="trivial", introduced_by=" ", dependency_path=None, - reachability="imported", + reachability="not_reachable", reachability_evidence=_DEP_EVIDENCE, contextual_cvss_breakdown=_DEP_CONTEXT, contextual_cvss_reasoning=_DEP_REASONING, @@ -618,7 +618,7 @@ async def test_dependency_report_with_no_contextual_impact_is_info( advisory_cvss=0.0, technical_analysis=None, fix_effort="low", - reachability="not_imported", + reachability="not_applicable", reachability_evidence="No file imports the package.", contextual_cvss_breakdown={**_DEP_CONTEXT, "availability": "N"}, contextual_cvss_reasoning="No application code imports the package.", @@ -649,7 +649,7 @@ async def test_dependency_report_records_reachability(report_state: ReportState) advisory_cvss=7.2, technical_analysis=None, fix_effort="low", - reachability="vulnerable_symbol_used", + reachability="reachable", reachability_evidence="src/render.ts:14 calls `_.template()`.", contextual_cvss_breakdown=_DEP_CONTEXT, contextual_cvss_reasoning=_DEP_REASONING, @@ -657,7 +657,7 @@ async def test_dependency_report_records_reachability(report_state: ReportState) assert result["success"] is True report = report_state.vulnerability_reports[0] - assert report["dependency_metadata"]["reachability"] == "vulnerable_symbol_used" + assert report["dependency_metadata"]["reachability"] == "reachable" assert ( report["dependency_metadata"]["reachability_evidence"] == "src/render.ts:14 calls `_.template()`." @@ -688,7 +688,7 @@ async def test_dependency_report_rejects_reachability_without_evidence( advisory_cvss=5.0, technical_analysis=None, fix_effort="low", - reachability="not_imported", + reachability="not_applicable", ) assert result["success"] is False @@ -725,7 +725,7 @@ async def test_dependency_report_rejects_unknown_reachability_level( assert not report_state.vulnerability_reports -async def test_dependency_report_records_unknown_reachability(report_state: ReportState) -> None: +async def test_dependency_report_requires_a_reachability_verdict(report_state: ReportState) -> None: result = await _do_create_dependency( title="CVE-2024-0001 in sample 1.0.0", description="Published advisory affects the pinned version.", @@ -748,10 +748,9 @@ async def test_dependency_report_records_unknown_reachability(report_state: Repo contextual_cvss_reasoning=_DEP_REASONING, ) - assert result["success"] is True, result - metadata = report_state.vulnerability_reports[0]["dependency_metadata"] - assert metadata["reachability"] == "unknown" - assert metadata["reachability_evidence"] == "Grep for the package found no import." + assert result["success"] is False, result + assert any("reachability is required" in e for e in result["errors"]) + assert not report_state.vulnerability_reports async def test_dependency_report_requires_advisory_cvss(report_state: ReportState) -> None: @@ -828,7 +827,7 @@ async def test_dependency_report_dedupe_candidate_includes_dependency_metadata( advisory_cvss=0.0, technical_analysis=None, fix_effort="low", - reachability="imported", + reachability="not_reachable", reachability_evidence=_DEP_EVIDENCE, contextual_cvss_breakdown=_DEP_CONTEXT, contextual_cvss_reasoning=_DEP_REASONING, @@ -847,7 +846,7 @@ async def test_dependency_report_dedupe_candidate_includes_dependency_metadata( "package_ecosystem": "npm", "manifest_path": "package-lock.json", "fixed_version": "1.0.1", - "reachability": "imported", + "reachability": "not_reachable", "reachability_evidence": _DEP_EVIDENCE, "contextual_cvss_breakdown": _DEP_CONTEXT, "contextual_cvss_score": pytest.approx(7.5, abs=0.05), @@ -1381,7 +1380,7 @@ async def test_dependency_report_computes_contextual_cvss( cwe="CWE-94", fix_effort="trivial", manifest_path="package-lock.json", - reachability="vulnerable_symbol_used", + reachability="reachable", reachability_evidence="scripts/import.py:88 calls `_.template()`.", contextual_cvss_breakdown=_CONTEXTUAL_BREAKDOWN, contextual_cvss_reasoning="Only scripts/import.py reaches the sink.", @@ -1421,7 +1420,7 @@ async def test_dependency_report_requires_contextual_breakdown( cwe="CWE-94", fix_effort="trivial", manifest_path="package-lock.json", - reachability="imported", + reachability="not_reachable", reachability_evidence=_DEP_EVIDENCE, ) assert result["success"] is False