diff --git a/strix/report/dedupe.py b/strix/report/dedupe.py index 23db066f..f848a6d6 100644 --- a/strix/report/dedupe.py +++ b/strix/report/dedupe.py @@ -183,6 +183,24 @@ def _dependency_identity(report: dict[str, Any]) -> tuple[str, str, str] | None: return cve, ecosystem, package_name +def _manifest_path(report: dict[str, Any]) -> str: + metadata = report.get("dependency_metadata") + if not isinstance(metadata, dict): + return "" + return str(metadata.get("manifest_path") or "").strip() + + +def _distinct_manifest_paths(candidate: dict[str, Any], report: dict[str, Any]) -> bool: + """Same CVE/package observed in two different manifests is two findings. + + Only applies when both sides carry a manifest_path; a missing path keeps + the legacy CVE/package/ecosystem identity. + """ + candidate_path = _manifest_path(candidate) + report_path = _manifest_path(report) + return bool(candidate_path and report_path and candidate_path != report_path) + + def _report_cve(report: dict[str, Any]) -> str: return str(report.get("cve") or "").strip().upper() @@ -228,6 +246,8 @@ def _check_dependency_duplicate( report_cve, report_ecosystem, report_package_name = report_identity if (report_cve, report_package_name) != (cve, package_name): continue + if _distinct_manifest_paths(candidate, report): + continue if report_ecosystem == ecosystem: return { "is_duplicate": True, diff --git a/strix/skills/custom/dependency_cve_scanning.md b/strix/skills/custom/dependency_cve_scanning.md index 766c41e1..1303fc0f 100644 --- a/strix/skills/custom/dependency_cve_scanning.md +++ b/strix/skills/custom/dependency_cve_scanning.md @@ -77,8 +77,10 @@ For each entry under `.Results[].Vulnerabilities[]` in `trivy-sca.json`, collect - `CVSS` — the published advisory base score - `PrimaryURL` / references — to verify the advisory -Deduplicate by `(CVE, PkgName, InstalledVersion)`. File one -`create_dependency_report` per CVE — do not batch multiple CVEs into one report. +Deduplicate by `(CVE, PkgName, Target)` — the same CVE/package observed in two +different manifests (e.g. two workspaces of a monorepo) is two findings, one +per manifest. File one `create_dependency_report` per CVE — do not batch +multiple CVEs into one report. ### Attribute transitive CVEs to the direct dependency diff --git a/tests/test_reporting_fields.py b/tests/test_reporting_fields.py index 6eda5667..b52fe4fd 100644 --- a/tests/test_reporting_fields.py +++ b/tests/test_reporting_fields.py @@ -653,6 +653,72 @@ async def test_dependency_dedupe_rejects_same_cve_package_identity() -> None: assert result["confidence"] == 1.0 +async def test_dependency_dedupe_keeps_findings_from_distinct_manifests() -> None: + existing = [ + { + "id": "vuln-0001", + "title": "CVE-2024-0001 in sample", + "cve": "CVE-2024-0001", + "dependency_metadata": { + "package_name": "sample", + "installed_version": "1.0.0", + "package_ecosystem": "npm", + "manifest_path": "services/api/package-lock.json", + }, + } + ] + candidate = { + "title": "CVE-2024-0001 in sample (web)", + "description": "Same advisory observed in a second workspace.", + "target": "repo/package.json", + "cve": "CVE-2024-0001", + "dependency_metadata": { + "package_name": "sample", + "installed_version": "1.0.0", + "package_ecosystem": "npm", + "manifest_path": "services/web/package-lock.json", + }, + } + + result = await check_duplicate(candidate, existing) + + assert result["is_duplicate"] is False + assert result["confidence"] == 1.0 + + +async def test_dependency_dedupe_rejects_same_manifest_identity() -> None: + existing = [ + { + "id": "vuln-0001", + "title": "CVE-2024-0001 in sample", + "cve": "CVE-2024-0001", + "dependency_metadata": { + "package_name": "sample", + "installed_version": "1.0.0", + "package_ecosystem": "npm", + "manifest_path": "services/api/package-lock.json", + }, + } + ] + candidate = { + "title": "CVE-2024-0001 in sample re-reported", + "description": "Same advisory, same manifest.", + "target": "repo/package.json", + "cve": "CVE-2024-0001", + "dependency_metadata": { + "package_name": "sample", + "installed_version": "1.0.0", + "package_ecosystem": "npm", + "manifest_path": "services/api/package-lock.json", + }, + } + + result = await check_duplicate(candidate, existing) + + assert result["is_duplicate"] is True + assert result["duplicate_id"] == "vuln-0001" + + async def test_dependency_dedupe_detects_legacy_same_cve_package() -> None: existing = [ {