From a9f9d41379851a629fd57df9acc0782b1c0ea4bf Mon Sep 17 00:00:00 2001 From: DarshanCode2005 Date: Sun, 8 Mar 2026 03:55:46 +0530 Subject: [PATCH 1/9] feat(shared): add CVSS version fallback in metric ingestion --- src/shared/fetchers.py | 22 +++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/src/shared/fetchers.py b/src/shared/fetchers.py index 3f069c1c7..a1c3593ae 100644 --- a/src/shared/fetchers.py +++ b/src/shared/fetchers.py @@ -97,15 +97,31 @@ def to_camel_case(name: str) -> str: def make_metric(data: dict[str, Any]) -> models.Metric: + cvss_preference = ("cvssV4_0", "cvssV3_1", "cvssV3_0") + + selected_format = "cvssV3_1" + raw_cvss: dict[str, Any] = {} + for cvss_format in cvss_preference: + candidate = data.get(cvss_format) + if isinstance(candidate, dict) and candidate: + selected_format = cvss_format + raw_cvss = candidate + break + ctx: dict[str, Any] = dict() - ctx["format"] = "cvssV3_1" - raw_cvss = data.get("cvssV3_1", {}) + ctx["format"] = selected_format ctx["raw_cvss_json"] = raw_cvss if raw_cvss: ctx["scope"] = raw_cvss.get("scope") ctx["vector_string"] = raw_cvss.get("vectorString") - ctx["base_score"] = float(raw_cvss.get("baseScore")) + base_score = raw_cvss.get("baseScore") + if base_score is not None: + try: + ctx["base_score"] = float(base_score) + except (TypeError, ValueError): + pass + ctx["base_severity"] = raw_cvss.get("baseSeverity") vector_fields = ( "attack_complexity", From 75796c93c3599824c486b94c3fdba9bd5e0689e7 Mon Sep 17 00:00:00 2001 From: DarshanCode2005 Date: Sun, 8 Mar 2026 03:57:23 +0530 Subject: [PATCH 2/9] fix(webview): handle non-CVSS3 vectors in severity badge --- src/webview/templatetags/viewutils.py | 40 +++++++++++++++++++-------- 1 file changed, 29 insertions(+), 11 deletions(-) diff --git a/src/webview/templatetags/viewutils.py b/src/webview/templatetags/viewutils.py index 1d3c18800..9adebdf70 100644 --- a/src/webview/templatetags/viewutils.py +++ b/src/webview/templatetags/viewutils.py @@ -127,18 +127,36 @@ def severity_badge(metrics: list[dict]) -> dict: For now we return the first metric that has a sane looking raw JSON field. """ for m in metrics: - if "raw_cvss_json" in m and "vectorString" in m.get("raw_cvss_json", {}): - parsed = CVSS3(m["raw_cvss_json"]["vectorString"]) - return { - "vectorString": m["raw_cvss_json"]["vectorString"], - "version": m["raw_cvss_json"]["version"], - "metrics": { - # XXX(@fricklerhandwerk): Yes, the *value* description is also indexed by *key*! - f"{METRICS_ABBREVIATIONS[k]} ({k})": f"{parsed.get_value_description(k)} ({v})" - for k, v in parsed.metrics.items() - if not k.startswith("M") # Don't display modified metrics - }, + raw_cvss = m.get("raw_cvss_json", {}) + if "vectorString" in raw_cvss: + result = { + "vectorString": raw_cvss["vectorString"], + "version": raw_cvss.get("version"), + "baseScore": raw_cvss.get("baseScore"), + "baseSeverity": raw_cvss.get("baseSeverity"), + "attackVector": raw_cvss.get("attackVector"), + "attackComplexity": raw_cvss.get("attackComplexity"), + "privilegesRequired": raw_cvss.get("privilegesRequired"), + "userInteraction": raw_cvss.get("userInteraction"), + "scope": raw_cvss.get("scope"), + "confidentialityImpact": raw_cvss.get("confidentialityImpact"), + "integrityImpact": raw_cvss.get("integrityImpact"), + "availabilityImpact": raw_cvss.get("availabilityImpact"), + "metrics": {}, } + + # Only CVSS3 vectors are supported by this parser. + try: + parsed = CVSS3(raw_cvss["vectorString"]) + except Exception: + return result + + result["metrics"] = { + f"{METRICS_ABBREVIATIONS[k]} ({k})": f"{parsed.get_value_description(k)} ({v})" + for k, v in parsed.metrics.items() + if not k.startswith("M") + } + return result return {} From 7ec51b08991eee2d5c45177d6299954b25571330 Mon Sep 17 00:00:00 2001 From: DarshanCode2005 Date: Sun, 8 Mar 2026 03:58:04 +0530 Subject: [PATCH 3/9] test(cvss): cover fallback ingestion and severity badge parsing --- src/shared/tests/test_fetchers.py | 47 +++++++++++++++++++++++++++++ src/webview/tests/test_viewutils.py | 40 ++++++++++++++++++++++++ 2 files changed, 87 insertions(+) create mode 100644 src/shared/tests/test_fetchers.py create mode 100644 src/webview/tests/test_viewutils.py diff --git a/src/shared/tests/test_fetchers.py b/src/shared/tests/test_fetchers.py new file mode 100644 index 000000000..bb1de15fd --- /dev/null +++ b/src/shared/tests/test_fetchers.py @@ -0,0 +1,47 @@ +from shared.fetchers import make_metric + + +def test_make_metric_uses_cvss_v4_0_when_v3_1_is_missing(db: None) -> None: + metric = make_metric( + { + "cvssV4_0": { + "version": "4.0", + "vectorString": "CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:H/VA:H/SC:N/SI:N/SA:N", + "baseScore": 9.3, + "baseSeverity": "CRITICAL", + } + } + ) + + assert metric.format == "cvssV4_0" + assert metric.raw_cvss_json["version"] == "4.0" + assert metric.vector_string == "CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:H/VA:H/SC:N/SI:N/SA:N" + assert metric.base_score == 9.3 + assert metric.base_severity == "CRITICAL" + + +def test_make_metric_uses_cvss_v3_0_when_v3_1_is_missing(db: None) -> None: + metric = make_metric( + { + "cvssV3_0": { + "version": "3.0", + "vectorString": "CVSS:3.0/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H", + "baseScore": "9.8", + "baseSeverity": "CRITICAL", + "attackVector": "NETWORK", + "attackComplexity": "LOW", + "privilegesRequired": "NONE", + "userInteraction": "NONE", + "scope": "UNCHANGED", + "confidentialityImpact": "HIGH", + "integrityImpact": "HIGH", + "availabilityImpact": "HIGH", + } + } + ) + + assert metric.format == "cvssV3_0" + assert metric.base_score == 9.8 + assert metric.base_severity == "CRITICAL" + assert metric.attack_vector == "NETWORK" + diff --git a/src/webview/tests/test_viewutils.py b/src/webview/tests/test_viewutils.py new file mode 100644 index 000000000..13c9ef158 --- /dev/null +++ b/src/webview/tests/test_viewutils.py @@ -0,0 +1,40 @@ +from webview.templatetags.viewutils import severity_badge + + +def test_severity_badge_handles_cvss_v4_vectors() -> None: + metric = severity_badge( + [ + { + "raw_cvss_json": { + "version": "4.0", + "vectorString": "CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:H/VA:H/SC:N/SI:N/SA:N", + "baseScore": 9.3, + "baseSeverity": "CRITICAL", + } + } + ] + ) + + assert metric["version"] == "4.0" + assert metric["baseScore"] == 9.3 + assert metric["baseSeverity"] == "CRITICAL" + assert metric["metrics"] == {} + + +def test_severity_badge_parses_cvss_v3_vectors() -> None: + metric = severity_badge( + [ + { + "raw_cvss_json": { + "version": "3.1", + "vectorString": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H", + "baseScore": 9.8, + "baseSeverity": "CRITICAL", + } + } + ] + ) + + assert metric["version"] == "3.1" + assert "Attack Vector (AV)" in metric["metrics"] + From 33e592c91bae1759b8d6f7b7ec69fd220edf06a2 Mon Sep 17 00:00:00 2001 From: DarshanCode2005 Date: Mon, 9 Mar 2026 12:52:44 +0530 Subject: [PATCH 4/9] feat(viewutils): simplify the parser format extraction and check --- src/webview/templatetags/viewutils.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/webview/templatetags/viewutils.py b/src/webview/templatetags/viewutils.py index 9adebdf70..05f6fd720 100644 --- a/src/webview/templatetags/viewutils.py +++ b/src/webview/templatetags/viewutils.py @@ -128,7 +128,7 @@ def severity_badge(metrics: list[dict]) -> dict: """ for m in metrics: raw_cvss = m.get("raw_cvss_json", {}) - if "vectorString" in raw_cvss: + if m["format"].startswith("cvssV3"): result = { "vectorString": raw_cvss["vectorString"], "version": raw_cvss.get("version"), @@ -146,10 +146,7 @@ def severity_badge(metrics: list[dict]) -> dict: } # Only CVSS3 vectors are supported by this parser. - try: - parsed = CVSS3(raw_cvss["vectorString"]) - except Exception: - return result + parsed = CVSS3(raw_cvss["vectorString"]) result["metrics"] = { f"{METRICS_ABBREVIATIONS[k]} ({k})": f"{parsed.get_value_description(k)} ({v})" From 1137d16abff756075028e30c6537c396cbb2b786 Mon Sep 17 00:00:00 2001 From: DarshanCode2005 Date: Mon, 9 Mar 2026 13:34:07 +0530 Subject: [PATCH 5/9] refactor(fetchers): simpliffy make_metric() --- src/shared/fetchers.py | 22 +++++++++------------- 1 file changed, 9 insertions(+), 13 deletions(-) diff --git a/src/shared/fetchers.py b/src/shared/fetchers.py index a1c3593ae..59dc9bdbc 100644 --- a/src/shared/fetchers.py +++ b/src/shared/fetchers.py @@ -99,28 +99,24 @@ def to_camel_case(name: str) -> str: def make_metric(data: dict[str, Any]) -> models.Metric: cvss_preference = ("cvssV4_0", "cvssV3_1", "cvssV3_0") - selected_format = "cvssV3_1" raw_cvss: dict[str, Any] = {} - for cvss_format in cvss_preference: - candidate = data.get(cvss_format) - if isinstance(candidate, dict) and candidate: - selected_format = cvss_format + selected_format = "" + + for version in cvss_preference: + candidate = data.get(version) + if candidate: raw_cvss = candidate + selected_format = version break - ctx: dict[str, Any] = dict() + ctx: dict[str, Any] = {} ctx["format"] = selected_format ctx["raw_cvss_json"] = raw_cvss if raw_cvss: ctx["scope"] = raw_cvss.get("scope") ctx["vector_string"] = raw_cvss.get("vectorString") - base_score = raw_cvss.get("baseScore") - if base_score is not None: - try: - ctx["base_score"] = float(base_score) - except (TypeError, ValueError): - pass + ctx["base_score"] = float(raw_cvss["baseScore"]) ctx["base_severity"] = raw_cvss.get("baseSeverity") vector_fields = ( @@ -137,7 +133,7 @@ def make_metric(data: dict[str, Any]) -> models.Metric: ctx[field] = raw_cvss.get(to_camel_case(field)) # TODO: Parse vector string into the various elements - # and verify conformance with the "parsed" fields for us. + # and verify conformance with the parsed fields. obj = models.Metric.objects.create(**ctx) obj.scenarios.set(map(make_description, data.get("scenarios", []))) From 47bb9407858bf8eeabd541989a69a6415605cd31 Mon Sep 17 00:00:00 2001 From: DarshanCode2005 Date: Mon, 9 Mar 2026 14:59:41 +0530 Subject: [PATCH 6/9] feat(viewutils): support CVSS v4 vectors in severity_badge --- src/webview/templatetags/viewutils.py | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/src/webview/templatetags/viewutils.py b/src/webview/templatetags/viewutils.py index 05f6fd720..d7c67f1cb 100644 --- a/src/webview/templatetags/viewutils.py +++ b/src/webview/templatetags/viewutils.py @@ -128,9 +128,10 @@ def severity_badge(metrics: list[dict]) -> dict: """ for m in metrics: raw_cvss = m.get("raw_cvss_json", {}) - if m["format"].startswith("cvssV3"): + fmt = m.get("format", "") + if fmt.startswith(("cvssV3", "cvssV4")): result = { - "vectorString": raw_cvss["vectorString"], + "vectorString": raw_cvss.get("vectorString"), "version": raw_cvss.get("version"), "baseScore": raw_cvss.get("baseScore"), "baseSeverity": raw_cvss.get("baseSeverity"), @@ -145,14 +146,15 @@ def severity_badge(metrics: list[dict]) -> dict: "metrics": {}, } - # Only CVSS3 vectors are supported by this parser. - parsed = CVSS3(raw_cvss["vectorString"]) + if fmt.startswith("cvssV3"): + # Only CVSS3 vectors are supported by this parser. + parsed = CVSS3(raw_cvss["vectorString"]) - result["metrics"] = { - f"{METRICS_ABBREVIATIONS[k]} ({k})": f"{parsed.get_value_description(k)} ({v})" - for k, v in parsed.metrics.items() - if not k.startswith("M") - } + result["metrics"] = { + f"{METRICS_ABBREVIATIONS[k]} ({k})": f"{parsed.get_value_description(k)} ({v})" + for k, v in parsed.metrics.items() + if not k.startswith("M") + } return result return {} From b1b89a8211b5a1ef9873efc9e268a4d557169008 Mon Sep 17 00:00:00 2001 From: DarshanCode2005 Date: Mon, 9 Mar 2026 15:00:20 +0530 Subject: [PATCH 7/9] refactor(tests): unify CVSS vector tests and add fixtures --- src/webview/tests/conftest.py | 26 +++++++++++++++ src/webview/tests/test_viewutils.py | 50 ++++++++++------------------- 2 files changed, 43 insertions(+), 33 deletions(-) diff --git a/src/webview/tests/conftest.py b/src/webview/tests/conftest.py index 4c9da3339..2f857133e 100644 --- a/src/webview/tests/conftest.py +++ b/src/webview/tests/conftest.py @@ -78,3 +78,29 @@ def wrapped(user: User) -> Generator[Page]: yield page return wrapped + + +@pytest.fixture +def cvss_v3_metric(): + return { + "format": "cvssV31", + "raw_cvss_json": { + "version": "3.1", + "vectorString": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H", + "baseScore": 9.8, + "baseSeverity": "CRITICAL", + }, + } + + +@pytest.fixture +def cvss_v4_metric(): + return { + "format": "cvssV40", + "raw_cvss_json": { + "version": "4.0", + "vectorString": "CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:H/VA:H/SC:N/SI:N/SA:N", + "baseScore": 9.3, + "baseSeverity": "CRITICAL", + }, + } diff --git a/src/webview/tests/test_viewutils.py b/src/webview/tests/test_viewutils.py index 13c9ef158..af4c4cdf4 100644 --- a/src/webview/tests/test_viewutils.py +++ b/src/webview/tests/test_viewutils.py @@ -1,40 +1,24 @@ from webview.templatetags.viewutils import severity_badge -def test_severity_badge_handles_cvss_v4_vectors() -> None: - metric = severity_badge( - [ - { - "raw_cvss_json": { - "version": "4.0", - "vectorString": "CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:H/VA:H/SC:N/SI:N/SA:N", - "baseScore": 9.3, - "baseSeverity": "CRITICAL", - } - } - ] - ) - - assert metric["version"] == "4.0" - assert metric["baseScore"] == 9.3 - assert metric["baseSeverity"] == "CRITICAL" - assert metric["metrics"] == {} +import pytest +from webview.templatetags.viewutils import severity_badge -def test_severity_badge_parses_cvss_v3_vectors() -> None: - metric = severity_badge( - [ - { - "raw_cvss_json": { - "version": "3.1", - "vectorString": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H", - "baseScore": 9.8, - "baseSeverity": "CRITICAL", - } - } - ] - ) +@pytest.mark.parametrize( + "metric_input, expected_version, expected_score", + [ + ("cvss_v3_metric", "3.1", 9.8), + ("cvss_v4_metric", "4.0", 9.3), + ], +) +def test_severity_badge_handles_cvss_versions( + request, metric_input, expected_version, expected_score +): + metric = severity_badge([request.getfixturevalue(metric_input)]) - assert metric["version"] == "3.1" - assert "Attack Vector (AV)" in metric["metrics"] + assert metric["version"] == expected_version + assert metric["baseScore"] == expected_score + assert metric["baseSeverity"] == "CRITICAL" + assert metric["vectorString"].startswith("CVSS:") From 47750107e14b0141cb70cf02d760830e8748a09f Mon Sep 17 00:00:00 2001 From: DarshanCode2005 Date: Mon, 9 Mar 2026 15:12:17 +0530 Subject: [PATCH 8/9] fix: remove float baseScore, present in Schema --- src/shared/fetchers.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/shared/fetchers.py b/src/shared/fetchers.py index 59dc9bdbc..b83ba095d 100644 --- a/src/shared/fetchers.py +++ b/src/shared/fetchers.py @@ -116,7 +116,7 @@ def make_metric(data: dict[str, Any]) -> models.Metric: if raw_cvss: ctx["scope"] = raw_cvss.get("scope") ctx["vector_string"] = raw_cvss.get("vectorString") - ctx["base_score"] = float(raw_cvss["baseScore"]) + ctx["base_score"] = raw_cvss["baseScore"] ctx["base_severity"] = raw_cvss.get("baseSeverity") vector_fields = ( @@ -133,7 +133,7 @@ def make_metric(data: dict[str, Any]) -> models.Metric: ctx[field] = raw_cvss.get(to_camel_case(field)) # TODO: Parse vector string into the various elements - # and verify conformance with the parsed fields. + # and verify conformance with the "parsed" fields for us. obj = models.Metric.objects.create(**ctx) obj.scenarios.set(map(make_description, data.get("scenarios", []))) From f360489218ded0979183e3d8dfe2dbbedbb03c15 Mon Sep 17 00:00:00 2001 From: DarshanCode2005 Date: Mon, 9 Mar 2026 21:35:03 +0530 Subject: [PATCH 9/9] refactor(viewutils): implement dictionary based lookup --- src/webview/templatetags/viewutils.py | 34 +++++++++++++++++---------- 1 file changed, 22 insertions(+), 12 deletions(-) diff --git a/src/webview/templatetags/viewutils.py b/src/webview/templatetags/viewutils.py index d7c67f1cb..2e09b46ad 100644 --- a/src/webview/templatetags/viewutils.py +++ b/src/webview/templatetags/viewutils.py @@ -3,8 +3,9 @@ from typing import Any, TypedDict from urllib.parse import quote, urlencode -from cvss import CVSS3 -from cvss.constants3 import METRICS_ABBREVIATIONS +from cvss import CVSS3, CVSS4 +from cvss.constants3 import METRICS_ABBREVIATIONS as METRICS_ABBREVIATIONS3 +from cvss.constants4 import METRICS_ABBREVIATIONS as METRICS_ABBREVIATIONS4 from django import template from django.conf import settings from django.template.context import Context @@ -121,6 +122,12 @@ def notifications_badge( return {"count": count, "oob_update": oob_update} +CVSS_PARSERS = { + "cvssV3": (CVSS3, METRICS_ABBREVIATIONS3), + "cvssV4": (CVSS4, METRICS_ABBREVIATIONS4), +} + + @register.inclusion_tag("components/severity_badge.html") def severity_badge(metrics: list[dict]) -> dict: """ @@ -129,7 +136,7 @@ def severity_badge(metrics: list[dict]) -> dict: for m in metrics: raw_cvss = m.get("raw_cvss_json", {}) fmt = m.get("format", "") - if fmt.startswith(("cvssV3", "cvssV4")): + if fmt.startswith(tuple(CVSS_PARSERS.keys())): result = { "vectorString": raw_cvss.get("vectorString"), "version": raw_cvss.get("version"), @@ -146,15 +153,18 @@ def severity_badge(metrics: list[dict]) -> dict: "metrics": {}, } - if fmt.startswith("cvssV3"): - # Only CVSS3 vectors are supported by this parser. - parsed = CVSS3(raw_cvss["vectorString"]) - - result["metrics"] = { - f"{METRICS_ABBREVIATIONS[k]} ({k})": f"{parsed.get_value_description(k)} ({v})" - for k, v in parsed.metrics.items() - if not k.startswith("M") - } + for prefix, (parser_class, abbrevs) in CVSS_PARSERS.items(): + if fmt.startswith(prefix): + parsed = parser_class(raw_cvss["vectorString"]) + break + else: + return result + + result["metrics"] = { + f"{abbrevs[k]} ({k})": f"{parsed.get_value_description(k)} ({v})" + for k, v in parsed.metrics.items() + if not k.startswith("M") + } return result return {}