diff --git a/strix/tools/reporting/tool.py b/strix/tools/reporting/tool.py index 45f4f6586..87185fc02 100644 --- a/strix/tools/reporting/tool.py +++ b/strix/tools/reporting/tool.py @@ -1883,7 +1883,7 @@ def _build_dependency_evidence( return evidence -async def _do_create_dependency( # noqa: PLR0912 +async def _do_create_dependency( # noqa: PLR0912, PLR0915 *, title: str, description: str, @@ -1905,6 +1905,8 @@ async def _do_create_dependency( # noqa: PLR0912 manifest_path: str | None = None, reachability: str = "unknown", reachability_evidence: str | None = None, + code_locations: list[dict[str, Any]] | None = None, + fix_verification: str | None = None, contextual_cvss_breakdown: dict[str, str] | None = None, contextual_cvss_reasoning: str | None = None, agent_id: str | None = None, @@ -1947,6 +1949,11 @@ async def _do_create_dependency( # noqa: PLR0912 if manifest_err: errors.append(manifest_err) + parsed_locations = _normalize_code_locations(code_locations) + if parsed_locations: + errors.extend(_validate_code_locations(parsed_locations)) + errors.extend(_validate_fix_verification(parsed_locations, fix_verification)) + reachability = (reachability or "unknown").strip().lower() if reachability not in _VALID_REACHABILITY: errors.append( @@ -2053,6 +2060,8 @@ async def _do_create_dependency( # noqa: PLR0912 cvss=cvss_score if advisory_cvss is not None else None, cve=parsed_cve, cwe=cwe, + code_locations=parsed_locations, + fix_verification=fix_verification, finding_class="dependency_cve", dependency_metadata=dependency_metadata, agent_id=agent_id if isinstance(agent_id, str) else None, @@ -2107,6 +2116,8 @@ async def create_dependency_report( dependency_path: str | None = None, reachability: str = "unknown", reachability_evidence: str | None = None, + code_locations: list[dict[str, Any]] | None = None, + fix_verification: str | None = None, contextual_cvss_breakdown: dict[str, str] | None = None, contextual_cvss_reasoning: str | None = None, ) -> str: @@ -2217,6 +2228,23 @@ async def create_dependency_report( 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. + 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``:: + + {"file": "src/api/client.ts", "start_line": 14, + "snippet": "const x = require('pkg')", "label": "imports it"} + + Cite the repo-relative ``file`` and the exact 1-based + ``start_line`` where ``snippet`` actually sits — verify against + the real file, never guess. A location may also propose + the fix itself: ``fix_before`` (verbatim current code, e.g. the + manifest pin line) + ``fix_after`` (same lines with the fixed + version) — that combination requires ``fix_verification``. + fix_verification: Required whenever any ``code_locations`` entry + carries ``fix_after``. Same bar as a normal finding: security + closure, bypass review, preserved behavior, and how each was + checked. contextual_cvss_breakdown: **Required.** Full CVSS v3.1 rating of this CVE **in this codebase** — the same 8-metric object as ``create_vulnerability_report``'s ``cvss_breakdown``: @@ -2277,6 +2305,8 @@ async def create_dependency_report( manifest_path=manifest_path, reachability=reachability, reachability_evidence=reachability_evidence, + code_locations=code_locations, + fix_verification=fix_verification, contextual_cvss_breakdown=contextual_cvss_breakdown, contextual_cvss_reasoning=contextual_cvss_reasoning, agent_id=agent_id, diff --git a/tests/test_reporting_fields.py b/tests/test_reporting_fields.py index 46d55af34..3f24014e5 100644 --- a/tests/test_reporting_fields.py +++ b/tests/test_reporting_fields.py @@ -407,6 +407,85 @@ async def test_dependency_report_records_transitive_chain(report_state: ReportSt ) +async def test_dependency_report_persists_code_locations(report_state: ReportState) -> None: + """Usage-site locations file onto a dependency finding like any other.""" + 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", + manifest_path="package-lock.json", + fixed_version="4.17.21", + cwe="CWE-94", + advisory_cvss=7.2, + technical_analysis=None, + fix_effort="trivial", + reachability="vulnerable_symbol_used", + reachability_evidence=_DEP_EVIDENCE, + code_locations=[ + { + "file": "src/render.ts", + "start_line": 14, + "end_line": 14, + "snippet": "const merge = require('lodash').merge", + "label": "imports the vulnerable package", + } + ], + contextual_cvss_breakdown=_DEP_CONTEXT, + contextual_cvss_reasoning=_DEP_REASONING, + ) + assert result["success"] is True + locs = report_state.vulnerability_reports[0]["code_locations"] + assert locs[0]["file"] == "src/render.ts" + assert locs[0]["label"] == "imports the vulnerable package" + + +async def test_dependency_report_fix_locations_require_verification( + report_state: ReportState, +) -> None: + """A location proposing a fix still requires fix_verification.""" + 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", + manifest_path="package-lock.json", + fixed_version="4.17.21", + cwe="CWE-94", + advisory_cvss=7.2, + technical_analysis=None, + fix_effort="trivial", + reachability="imported", + reachability_evidence=_DEP_EVIDENCE, + code_locations=[ + { + "file": "package-lock.json", + "start_line": 10, + "end_line": 10, + "fix_before": '"lodash": "4.17.20"', + "fix_after": '"lodash": "4.17.21"', + } + ], + contextual_cvss_breakdown=_DEP_CONTEXT, + contextual_cvss_reasoning=_DEP_REASONING, + ) + assert result["success"] is False + assert any("fix_verification" in e for e in result["errors"]) + assert not report_state.vulnerability_reports + + async def test_dependency_report_omits_blank_chain_fields(report_state: ReportState) -> None: result = await _do_create_dependency( title="CVE-2024-0001 in sample 1.0.0",