Compare commits

...
5 changed files with 417 additions and 23 deletions
+21
View File
@@ -133,6 +133,27 @@ def format_vulnerability_report(report: dict[str, Any]) -> Text: # noqa: PLR091
text.append("CVSS Vector: ", style=field_style)
text.append("/".join(cvss_parts), style="dim")
dependency_metadata = report.get("dependency_metadata") or {}
if dependency_metadata:
contextual_vector = dependency_metadata.get("contextual_cvss_vector")
if contextual_vector:
text.append("\n\n")
text.append("Contextual CVSS Vector: ", style=field_style)
text.append(contextual_vector, style="dim")
advisory_cvss = dependency_metadata.get("advisory_cvss")
if advisory_cvss is not None and advisory_cvss != report.get("cvss"):
text.append("\n\n")
text.append("Advisory CVSS: ", style=field_style)
text.append(f"{float(advisory_cvss):.1f}", style="dim")
contextual_reasoning = dependency_metadata.get("contextual_cvss_reasoning")
if contextual_reasoning:
text.append("\n\n")
text.append("Contextual CVSS Reasoning", style=field_style)
text.append("\n")
text.append(contextual_reasoning)
description = report.get("description")
if description:
text.append("\n\n")
+10
View File
@@ -215,6 +215,11 @@ def render_vulnerability_md(report: dict[str, Any]) -> str: # noqa: PLR0912, PL
cvss = report.get("cvss")
if cvss is not None:
metadata.append(("CVSS", cvss))
advisory_cvss = dep_meta.get("advisory_cvss")
if advisory_cvss is not None and advisory_cvss != cvss:
metadata.append(("Advisory CVSS", advisory_cvss))
if dep_meta.get("contextual_cvss_vector"):
metadata.append(("Contextual CVSS Vector", dep_meta["contextual_cvss_vector"]))
if report.get("fix_effort"):
metadata.append(("Fix Effort", str(report["fix_effort"]).title()))
for label, value in metadata:
@@ -241,6 +246,11 @@ def render_vulnerability_md(report: dict[str, Any]) -> str: # noqa: PLR0912, PL
lines.append(str(report["technical_analysis"]))
lines.append("")
if dep_meta.get("contextual_cvss_reasoning"):
lines.append("## Contextual CVSS\n")
lines.append(str(dep_meta["contextual_cvss_reasoning"]))
lines.append("")
if report.get("poc_description") or report.get("poc_script_code"):
lines.append("## Proof of Concept\n")
if report.get("poc_description"):
+78 -7
View File
@@ -161,7 +161,23 @@ fi
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. If the analysis was not performed or is inconclusive (obfuscated code,
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`.
@@ -225,15 +241,68 @@ findings and rejects empty PoC fields):
installed/affected version, fixed version, lockfile path, and the relevant
trivy output excerpt.
- **Always set `advisory_cvss` to the published advisory base score (0.010.0).**
Severity is derived *solely* from this number: read it off the advisory (`CVSS`
in trivy output, or the NVD/GHSA page) and pass the real value. The tool rejects
a call that omits it, because guessing a score both inflates low CVEs and
deflates critical ones.
It is the published reference, and it rates the finding whenever you give no
contextual breakdown: read it off the advisory (`CVSS` in trivy output, or the
NVD/GHSA page) and pass the real value. The tool rejects a call that omits it,
because guessing a score both inflates low CVEs and deflates critical ones.
- Set `cwe` to the most specific `CWE-NNN` when the advisory names one.
- 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;
use `assumptions` for anything softer (confidence, caveats, analysis limits).
- Set `contextual_cvss_breakdown` + `contextual_cvss_reasoning` when this
codebase clearly changes the risk the published score describes (see below).
### Contextual CVSS
The published score rates the CVE in the abstract. `contextual_cvss_breakdown`
rates it **here**, in this codebase — the same 8-metric CVSS v3.1 object as a
normal finding's `cvss_breakdown` (`attack_vector`, `attack_complexity`,
`privileges_required`, `user_interaction`, `scope`, `confidentiality`,
`integrity`, `availability`). You never pass a score: the contextual score and
vector are computed from the breakdown, and when you provide one it determines
the finding's severity. `advisory_cvss` stays the published reference.
Start from the advisory's own published metrics and change only what your
evidence proves is different in this codebase:
- `attack_vector` `N`/`A`/`L`/`P` — as deployed. A library reached only by a
local CLI is `L`, not `N`.
- `attack_complexity` `L`/`H` — raise to `H` when the vulnerable path needs a
precondition the code enforces (input validation, a non-default flag, an
internal-only route).
- `privileges_required` `N`/`L`/`H`, `user_interaction` `N`/`R` — what this
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.
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
from that chain: `attack_vector`, `privileges_required`, and `user_interaction`
come from what the source requires; `attack_complexity` comes from the
preconditions the hops enforce; `confidentiality`, `integrity`, and
`availability` come from the data and privileges available at the sink.
No trace, no contextual breakdown: if you did not reach a symbol hit, or you
could not follow a hop, omit the contextual fields instead of guessing.
`contextual_cvss_reasoning` is required with the breakdown. Write two to four
sentences that another engineer can check without opening the repository. Name
the chain hop by hop as `entry point -> intermediate call -> package call`, with
a repository-relative `file:line` for each hop, say who controls the input, and
say what the contextual rating changes. Example: lowering `attack_vector` to
`L` and `confidentiality` to `L` with "The only caller of `yaml.load` is
`parse_manifest` in `scripts/import.py:88`, which `cli/commands.py:212` invokes
for an operator-supplied path behind the `--allow-unsafe-import` flag that
`deploy/prod.yaml` never sets. No HTTP route reaches that function, so an
attacker must already hold shell access on the job host, and the parsed data is
build metadata rather than customer records."
Omit all the contextual fields when the published rating already fits, and when
the evidence is thin. A contextual rating is a claim you must be able to
defend, and it never replaces `advisory_cvss` as the published reference.
Verify the CVE with `web_search` when available before reporting. Never guess or
hallucinate a CVE id.
@@ -244,10 +313,12 @@ hallucinate a CVE id.
`create_dependency_report`.
- Do not report a finding without a verified CVE id.
- Do not batch multiple CVEs into one report.
- Do not omit `advisory_cvss` — the tool rejects it, and it is the single input
that determines dependency severity.
- Do not omit `advisory_cvss` — the tool rejects it, and it rates every finding
that carries no contextual breakdown.
- 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 send `contextual_cvss_breakdown` without evidence-backed reasoning, and
do not use it to quietly de-rate a CVE you simply could not analyze.
+152 -15
View File
@@ -749,6 +749,61 @@ def _validate_manifest_path(manifest_path: str | None) -> str | None:
return None
_MAX_CONTEXTUAL_REASONING_CHARS = 2000
def _validate_contextual_cvss(
breakdown: dict[str, str] | None,
reasoning: str | None,
) -> list[str]:
errors: list[str] = []
if breakdown:
for name, valid in _CVSS_VALID.items():
value = breakdown.get(name)
if value not in valid:
errors.append(
f"Invalid contextual_cvss_breakdown {name}: {value}. Must be one of: {valid}"
)
if not (reasoning or "").strip():
errors.append(
"contextual_cvss_reasoning is required when contextual_cvss_breakdown is "
"set: state what you observed in this codebase that justifies the "
"contextual rating. A contextual score with no reasoning is not shown."
)
return errors
def _validate_advisory_cvss(advisory_cvss: float | None) -> str | None:
if advisory_cvss is None:
return (
"advisory_cvss is required: read the published advisory base score "
"(0.0-10.0) off the advisory (trivy CVSS / NVD / GHSA). It is the "
"published reference the finding is rated against — do not omit it "
"or the finding cannot be rated."
)
if not 0.0 <= advisory_cvss <= 10.0:
return f"advisory_cvss must be between 0.0 and 10.0, got {advisory_cvss}"
return None
def _resolve_dependency_rating(
advisory_cvss: float | None,
contextual_cvss_breakdown: dict[str, str] | None,
) -> tuple[float | None, str, float | None, str | None]:
"""Rate the finding.
A contextual breakdown works exactly like a normal finding's
``cvss_breakdown``: the agent supplies the 8 metrics as observed in this
codebase and the score/vector are computed from them. When provided it
rates the finding; the advisory score stays as the published reference.
"""
if contextual_cvss_breakdown:
score, severity, vector = _calculate_cvss(contextual_cvss_breakdown)
return score, severity, score, vector
score, severity = _dependency_severity(advisory_cvss)
return score, severity, None, None
def _build_dependency_metadata(
*,
package_name: str,
@@ -760,11 +815,18 @@ def _build_dependency_metadata(
manifest_path: str | None = None,
reachability: str | None = None,
reachability_evidence: str | None = None,
) -> dict[str, str]:
metadata = {
advisory_cvss: float | None = None,
contextual_cvss_breakdown: dict[str, str] | None = None,
contextual_cvss_score: float | None = None,
contextual_cvss_vector: str | None = None,
contextual_cvss_reasoning: str | None = None,
) -> dict[str, Any]:
metadata: dict[str, Any] = {
"package_name": package_name.strip(),
"installed_version": installed_version.strip(),
}
if advisory_cvss is not None:
metadata["advisory_cvss"] = advisory_cvss
if package_ecosystem and package_ecosystem.strip():
metadata["package_ecosystem"] = package_ecosystem.strip()
if manifest_path and manifest_path.strip():
@@ -781,6 +843,20 @@ def _build_dependency_metadata(
metadata["reachability"] = reachability.strip()
if reachability_evidence and reachability_evidence.strip():
metadata["reachability_evidence"] = reachability_evidence.strip()
# Contextual CVSS is only meaningful as the full breakdown, its computed
# score/vector, and the reasoning a reader can check — an incomplete set
# is dropped.
reasoning = str(contextual_cvss_reasoning or "").strip()
if (
contextual_cvss_breakdown
and contextual_cvss_score is not None
and contextual_cvss_vector
and reasoning
):
metadata["contextual_cvss_breakdown"] = contextual_cvss_breakdown
metadata["contextual_cvss_score"] = contextual_cvss_score
metadata["contextual_cvss_vector"] = contextual_cvss_vector
metadata["contextual_cvss_reasoning"] = reasoning[:_MAX_CONTEXTUAL_REASONING_CHARS]
return metadata
@@ -852,6 +928,8 @@ async def _do_create_dependency( # noqa: PLR0912
manifest_path: str | None = None,
reachability: str = "unknown",
reachability_evidence: str | None = None,
contextual_cvss_breakdown: dict[str, str] | None = None,
contextual_cvss_reasoning: str | None = None,
agent_id: str | None = None,
agent_name: str | None = None,
) -> dict[str, Any]:
@@ -904,19 +982,21 @@ async def _do_create_dependency( # noqa: PLR0912
"govulncheck call path). Never claim a reachability level without evidence."
)
if advisory_cvss is None:
errors.append(
"advisory_cvss is required: read the published advisory base score "
"(0.0-10.0) off the advisory (trivy CVSS / NVD / GHSA). Severity is "
"derived solely from it — do not omit it or the finding cannot be rated."
)
elif not 0.0 <= advisory_cvss <= 10.0:
errors.append(f"advisory_cvss must be between 0.0 and 10.0, got {advisory_cvss}")
errors.extend(_validate_contextual_cvss(contextual_cvss_breakdown, contextual_cvss_reasoning))
advisory_err = _validate_advisory_cvss(advisory_cvss)
if advisory_err:
errors.append(advisory_err)
if errors:
return {"success": False, "error": "Validation failed", "errors": errors}
cvss_score, severity = _dependency_severity(advisory_cvss)
try:
cvss_score, severity, contextual_score, contextual_vector = _resolve_dependency_rating(
advisory_cvss, contextual_cvss_breakdown
)
except ValueError as exc:
return {"success": False, "error": "Validation failed", "errors": [str(exc)]}
dependency_metadata = _build_dependency_metadata(
package_name=package_name,
installed_version=installed_version,
@@ -927,6 +1007,11 @@ async def _do_create_dependency( # noqa: PLR0912
manifest_path=manifest_path,
reachability=reachability,
reachability_evidence=reachability_evidence,
advisory_cvss=advisory_cvss,
contextual_cvss_breakdown=contextual_cvss_breakdown,
contextual_cvss_score=contextual_score,
contextual_cvss_vector=contextual_vector,
contextual_cvss_reasoning=contextual_cvss_reasoning,
)
evidence = _build_dependency_evidence(
cve=parsed_cve,
@@ -1038,6 +1123,8 @@ async def create_dependency_report(
dependency_path: str | None = None,
reachability: str = "unknown",
reachability_evidence: str | None = None,
contextual_cvss_breakdown: dict[str, str] | None = None,
contextual_cvss_reasoning: str | None = None,
) -> str:
"""File a known-CVE dependency (SCA) finding — one report per CVE x package.
@@ -1080,8 +1167,10 @@ async def create_dependency_report(
proved a path from application code to the vulnerable function.
- ``unknown`` — usage analysis was not performed or was inconclusive.
Severity is still derived solely from ``advisory_cvss`` — the
reachability level never changes the rating, only prioritization.
Severity comes from ``contextual_cvss_breakdown`` when you provide one
(computed exactly like a normal finding's ``cvss_breakdown``), otherwise
from ``advisory_cvss``. The reachability level alone never changes the
rating, only prioritization.
**Formatting**: use markdown in text fields (``**bold**``, ``inline
code`` for package/version identifiers, fenced code blocks for
@@ -1102,8 +1191,9 @@ async def create_dependency_report(
cwe: ``CWE-NNN`` (most specific) if certain, else omit.
advisory_cvss: **Required.** Published advisory base score
(0.0-10.0) — read it off the advisory (trivy CVSS / NVD / GHSA).
Severity is derived solely from this score, so it must be the
real published value; do not guess or omit it.
It is the published reference the finding is rated against and
rates the finding whenever you give no contextual breakdown, so
it must be the real published value; do not guess or omit it.
technical_analysis: Optional deeper mechanism/root-cause detail.
fix_effort: One of ``trivial`` / ``low`` / ``medium`` / ``high``
(dependency upgrades are usually ``trivial``/``low``).
@@ -1131,6 +1221,51 @@ async def create_dependency_report(
(required for any level other than ``unknown``): 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
**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
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.
contextual_cvss_breakdown: Optional full CVSS v3.1 rating of this
CVE **in this codebase** — the same 8-metric object as
``create_vulnerability_report``'s ``cvss_breakdown``:
``attack_vector`` (N/A/L/P), ``attack_complexity`` (L/H),
``privileges_required`` (N/L/H), ``user_interaction`` (N/R),
``scope`` (U/C), ``confidentiality`` / ``integrity`` /
``availability`` (N/L/H). All 8 metrics are required when the
field is set, and the contextual score/vector are computed
from them — you never supply a score. Start from the
advisory's published metrics and change only what the
**source-to-sink trace** you recorded in
``reachability_evidence`` proves is different here: derive
``attack_vector`` / ``privileges_required`` /
``user_interaction`` from what the entry point actually
requires, ``attack_complexity`` from the preconditions the
hops enforce, and the impact metrics from the data and
privileges reachable at the sink. When provided, this rating
determines the finding's severity; ``advisory_cvss`` stays as
the published reference. Omit the field when the trace does
not change the published rating, or when you could not
complete the trace.
contextual_cvss_reasoning: **Required whenever**
``contextual_cvss_breakdown`` is set. Two to four detailed
sentences that a reviewer can verify without opening the repo:
how the application uses the package, which call sites or
configuration you inspected (repo-relative ``file:line``),
which input reaches the vulnerable code and whether an
attacker controls it, and what the adjustment therefore
changes. State the source-to-sink chain explicitly, hop by
hop, as ``entry point -> intermediate call -> package call``
with a ``file:line`` for each hop. Cite concrete evidence,
never a generic statement such as "low risk". The user reads
this text next to the adjusted score, so an adjustment
without it is discarded.
"""
agent_id, agent_name = _caller_identity(ctx)
@@ -1155,6 +1290,8 @@ async def create_dependency_report(
manifest_path=manifest_path,
reachability=reachability,
reachability_evidence=reachability_evidence,
contextual_cvss_breakdown=contextual_cvss_breakdown,
contextual_cvss_reasoning=contextual_cvss_reasoning,
agent_id=agent_id,
agent_name=agent_name,
)
+156 -1
View File
@@ -160,6 +160,7 @@ async def test_dependency_report_sets_class_and_metadata(report_state: ReportSta
assert report["dependency_metadata"] == {
"package_name": "lodash",
"installed_version": "4.17.20",
"advisory_cvss": 7.2,
"package_ecosystem": "npm",
"manifest_path": "package-lock.json",
"fixed_version": "4.17.21",
@@ -372,7 +373,7 @@ async def test_dependency_report_omits_unknown_reachability(report_state: Report
fix_effort="low",
)
assert result["success"] is True
assert result["success"] is True, result
metadata = report_state.vulnerability_reports[0]["dependency_metadata"]
assert "reachability" not in metadata
assert "reachability_evidence" not in metadata
@@ -463,6 +464,7 @@ async def test_dependency_report_dedupe_candidate_includes_dependency_metadata(
"dependency_metadata": {
"package_name": "sample",
"installed_version": "1.0.0",
"advisory_cvss": 0.0,
"package_ecosystem": "npm",
"manifest_path": "package-lock.json",
"fixed_version": "1.0.1",
@@ -877,3 +879,156 @@ def test_vuln_tool_exposes_new_params() -> None:
dep_required = create_dependency_report.params_json_schema["required"]
assert "package_ecosystem" in dep_required
assert "advisory_cvss" in dep_required
def test_dep_tool_exposes_contextual_cvss_params() -> None:
dep_props = create_dependency_report.params_json_schema["properties"]
for field in (
"contextual_cvss_breakdown",
"contextual_cvss_reasoning",
):
assert field in dep_props
assert "source-to-sink" in dep_props["contextual_cvss_breakdown"]["description"].lower()
assert "source-to-sink" in dep_props["reachability_evidence"]["description"].lower()
assert "file:line" in dep_props["contextual_cvss_reasoning"]["description"].lower()
_CONTEXTUAL_BREAKDOWN = {
"attack_vector": "L",
"attack_complexity": "H",
"privileges_required": "H",
"user_interaction": "N",
"scope": "U",
"confidentiality": "L",
"integrity": "L",
"availability": "N",
}
@pytest.mark.asyncio
async def test_dependency_report_computes_contextual_cvss(
report_state: ReportState,
) -> None:
result = await _do_create_dependency(
title="CVE-2021-23337 in lodash 4.17.20",
description="Command injection via template.",
target="repo/package.json",
cve="CVE-2021-23337",
package_name="lodash",
installed_version="4.17.20",
impact="Arbitrary command execution.",
remediation_steps="Upgrade to 4.17.21.",
assumptions="Assumes the template sink is reachable.",
package_ecosystem="npm",
advisory_cvss=7.2,
technical_analysis=None,
fixed_version="4.17.21",
cwe="CWE-94",
fix_effort="trivial",
manifest_path="package-lock.json",
contextual_cvss_breakdown=_CONTEXTUAL_BREAKDOWN,
contextual_cvss_reasoning="Only scripts/import.py reaches the sink.",
)
assert result["success"] is True, result
report = report_state.vulnerability_reports[0]
metadata = report["dependency_metadata"]
assert metadata["advisory_cvss"] == 7.2
assert metadata["contextual_cvss_breakdown"] == _CONTEXTUAL_BREAKDOWN
assert metadata["contextual_cvss_vector"] == ("CVSS:3.1/AV:L/AC:H/PR:H/UI:N/S:U/C:L/I:L/A:N")
assert metadata["contextual_cvss_score"] == pytest.approx(3.0, abs=0.05)
assert metadata["contextual_cvss_reasoning"] == "Only scripts/import.py reaches the sink."
# The contextual rating determines the finding's score/severity, exactly
# like a normal finding's cvss_breakdown.
assert report["cvss"] == metadata["contextual_cvss_score"]
assert report["severity"] == "low"
@pytest.mark.asyncio
async def test_dependency_report_rates_from_advisory_without_contextual(
report_state: ReportState,
) -> None:
result = await _do_create_dependency(
title="CVE-2021-23337 in lodash 4.17.20",
description="Command injection via template.",
target="repo/package.json",
cve="CVE-2021-23337",
package_name="lodash",
installed_version="4.17.20",
impact="Arbitrary command execution.",
remediation_steps="Upgrade to 4.17.21.",
assumptions="Assumes the template sink is reachable.",
package_ecosystem="npm",
advisory_cvss=7.2,
technical_analysis=None,
fixed_version="4.17.21",
cwe="CWE-94",
fix_effort="trivial",
manifest_path="package-lock.json",
)
assert result["success"] is True, result
report = report_state.vulnerability_reports[0]
assert report["cvss"] == 7.2
assert report["severity"] == "high"
metadata = report["dependency_metadata"]
assert metadata["advisory_cvss"] == 7.2
assert "contextual_cvss_breakdown" not in metadata
assert "contextual_cvss_score" not in metadata
@pytest.mark.asyncio
async def test_dependency_report_rejects_incomplete_contextual_breakdown(
report_state: ReportState,
) -> None:
result = await _do_create_dependency(
title="CVE-2021-23337 in lodash 4.17.20",
description="Command injection via template.",
target="repo/package.json",
cve="CVE-2021-23337",
package_name="lodash",
installed_version="4.17.20",
impact="Arbitrary command execution.",
remediation_steps="Upgrade to 4.17.21.",
assumptions="Assumes the template sink is reachable.",
package_ecosystem="npm",
advisory_cvss=7.2,
technical_analysis=None,
fixed_version="4.17.21",
cwe="CWE-94",
fix_effort="trivial",
manifest_path="package-lock.json",
contextual_cvss_breakdown={"attack_vector": "L", "attack_complexity": "Z"},
contextual_cvss_reasoning="Only scripts/import.py reaches the sink.",
)
assert result["success"] is False
assert any("attack_complexity" in error for error in result["errors"])
assert any("privileges_required" in error for error in result["errors"])
assert report_state.vulnerability_reports == []
@pytest.mark.asyncio
async def test_dependency_report_rejects_contextual_breakdown_without_reasoning(
report_state: ReportState,
) -> None:
result = await _do_create_dependency(
title="CVE-2021-23337 in lodash 4.17.20",
description="Command injection via template.",
target="repo/package.json",
cve="CVE-2021-23337",
package_name="lodash",
installed_version="4.17.20",
impact="Arbitrary command execution.",
remediation_steps="Upgrade to 4.17.21.",
assumptions="Assumes the template sink is reachable.",
package_ecosystem="npm",
advisory_cvss=7.2,
technical_analysis=None,
fixed_version="4.17.21",
cwe="CWE-94",
fix_effort="trivial",
manifest_path="package-lock.json",
contextual_cvss_breakdown=_CONTEXTUAL_BREAKDOWN,
contextual_cvss_reasoning=" ",
)
assert result["success"] is False
assert any("contextual_cvss_reasoning is required" in error for error in result["errors"])
assert report_state.vulnerability_reports == []