mirror of
https://github.com/usestrix/strix.git
synced 2026-11-09 05:19:39 -10:00
Compare commits
4
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ba54ed21d0 | ||
|
|
6b0bcb2134 | ||
|
|
44611c5e97 | ||
|
|
e0bc132b70 |
No files matched your search
@@ -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
|
||||
|
||||
@@ -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``::
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in new issue
Block a user