From f1e31434c9a0132c54b45befbfeb11ab43dc9e5f Mon Sep 17 00:00:00 2001 From: jav1er8 <93607250+jav1er8@users.noreply.github.com> Date: Fri, 23 Oct 2026 19:49:05 +0200 Subject: [PATCH] fix(sarif): link CVE-keyed rules to NVD and tag CWE rules for code scanning Only CWE-keyed rules carried a `helpUri`; a rule keyed on a CVE (a finding with a CVE but no CWE) gave the reviewer no reference link. Such rules now link to their NVD entry when the id is a well-formed CVE. CWE rules also get an `external/cwe/cwe-NNN` tag next to the existing `CWE-NNN` one. That is the form GitHub code scanning recognises as a CWE reference, so alerts can be filtered by CWE there. The module docstring now states that `properties.strix.severity` is the only lossless severity channel, since the five Strix severities collapse to three SARIF levels by design. Refs #1576 --- strix/report/sarif.py | 13 ++++++++++++- tests/test_sarif.py | 28 ++++++++++++++++++++++++++++ 2 files changed, 40 insertions(+), 1 deletion(-) diff --git a/strix/report/sarif.py b/strix/report/sarif.py index 821fffbc0..c605b67e5 100644 --- a/strix/report/sarif.py +++ b/strix/report/sarif.py @@ -21,7 +21,12 @@ Design notes: * SARIF only has three levels (error / warning / note). Strix's five severities collapse into them. The raw severity label and CVSS score survive in ``result.properties.strix`` for downstream tools that can - distinguish CRITICAL vs HIGH. + distinguish CRITICAL vs HIGH; ``properties.strix.severity`` is the only + lossless severity channel in the document. + * Rules link to their reference page through ``helpUri``: the CWE + definition for CWE-keyed rules, the NVD entry for CVE-keyed ones. CWE + rules are also tagged ``external/cwe/cwe-NNN``, the form GitHub + code-scanning recognises as a CWE reference. * GitHub code-scanning uses ``rule.properties['security-severity']`` (a 0.0-10.0 string) to rank alerts. We populate it from CVSS when available, otherwise from a conservative label -> score map. @@ -75,6 +80,9 @@ TOOL_INFORMATION_URI = "https://strix.ai" # findings instead of re-orphaning them on every run. _SYNTHETIC_LOCATION_URI = "SECURITY.md" +# CVE-keyed rules link to their NVD entry when the id is well formed. +_CVE_ID = re.compile(r"CVE-\d{4}-\d{4,}", re.IGNORECASE) + # SARIF only has three result levels; Strix's five severities collapse here. # Original label survives in ``result.properties.strix.severity``. @@ -1091,6 +1099,7 @@ def _rule_tags(rule_id: str, report: dict[str, Any]) -> list[str]: tags: list[str] = ["security"] if rule_id.startswith("CWE-"): tags.append(rule_id) + tags.append(f"external/cwe/{rule_id.lower()}") cve = _string_value(report.get("cve")) if cve and cve not in tags: tags.append(cve) @@ -1107,6 +1116,8 @@ def _rule_tags(rule_id: str, report: dict[str, Any]) -> list[str]: def _help_uri_for(rule_id: str) -> str | None: if rule_id.startswith("CWE-"): return f"https://cwe.mitre.org/data/definitions/{rule_id.removeprefix('CWE-')}.html" + if _CVE_ID.fullmatch(rule_id): + return f"https://nvd.nist.gov/vuln/detail/{rule_id.upper()}" return None diff --git a/tests/test_sarif.py b/tests/test_sarif.py index 61ffc39bb..113c7ea74 100644 --- a/tests/test_sarif.py +++ b/tests/test_sarif.py @@ -371,3 +371,31 @@ def test_calibration_metadata_survives_into_result_properties(tmp_path: Path) -> assert strix["confidence_rationale"] == "Reproduced once out of three attempts." assert strix["severity_change_conditions"] == "Critical if the WAF rule is removed." assert strix["fix_verification"] == "Not retested." + + +def _rules(run_dir: Path) -> list[dict[str, Any]]: + rules: list[dict[str, Any]] = _read(run_dir)["runs"][0]["tool"]["driver"]["rules"] + return rules + + +def test_write_sarif_cwe_rule_links_to_the_cwe_definition(tmp_path: Path) -> None: + write_sarif(tmp_path, [_finding(cwe="CWE-639")]) + rule = _rules(tmp_path)[0] + assert rule["id"] == "CWE-639" + assert rule["helpUri"] == "https://cwe.mitre.org/data/definitions/639.html" + # GitHub code scanning reads CWE references from ``external/cwe/cwe-NNN`` tags. + assert "external/cwe/cwe-639" in rule["properties"]["tags"] + assert rule["defaultConfiguration"] == {"level": "error"} + + +def test_write_sarif_cve_rule_links_to_nvd(tmp_path: Path) -> None: + write_sarif(tmp_path, [_finding(cwe=None, cve="CVE-2021-44228")]) + rule = _rules(tmp_path)[0] + assert rule["id"] == "CVE-2021-44228" + assert rule["helpUri"] == "https://nvd.nist.gov/vuln/detail/CVE-2021-44228" + assert not any(tag.startswith("external/cwe/") for tag in rule["properties"]["tags"]) + + +def test_write_sarif_rule_without_a_reference_id_has_no_help_uri(tmp_path: Path) -> None: + write_sarif(tmp_path, [_finding(cwe=None, cve="not-a-cve")]) + assert "helpUri" not in _rules(tmp_path)[0]