Compare commits

...
Author SHA1 Message Date
yoni ba54ed21d0 Revert "skills: a loaded package is never not_applicable"
This reverts commit 6b0bcb2134.
2026-11-09 06:21:35 +00:00
yoni 6b0bcb2134 skills: a loaded package is never not_applicable 2026-11-09 02:25:28 +00:00
yoni 44611c5e97 reporting: neutral not_applicable wording 2026-11-09 00:49:46 +00:00
yoni e0bc132b70 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.
2026-11-09 00:45:23 +00:00
3 changed files with 168 additions and 166 deletions

No files matched your search

+97 -94
View File
@@ -114,99 +114,103 @@ not just "upgrade <vulnerable pkg> to <fixed>".
### 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
+51 -51
View File
@@ -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,13 @@ 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 vulnerable package is not loaded, "
"or the installed copy is not affected"
),
}
@@ -1899,7 +1893,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 +1949,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 +2114,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 +2144,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 +2211,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``::
+20 -21
View File
@@ -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="[email protected]",
dependency_path="[email protected] > [email protected] > [email protected]",
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