Skip to content
20 changes: 16 additions & 4 deletions src/shared/fetchers.py
Original file line number Diff line number Diff line change
Expand Up @@ -97,15 +97,27 @@ def to_camel_case(name: str) -> str:


def make_metric(data: dict[str, Any]) -> models.Metric:
ctx: dict[str, Any] = dict()
ctx["format"] = "cvssV3_1"
raw_cvss = data.get("cvssV3_1", {})
cvss_preference = ("cvssV4_0", "cvssV3_1", "cvssV3_0")

raw_cvss: dict[str, Any] = {}
selected_format = ""

for version in cvss_preference:
candidate = data.get(version)
if candidate:
raw_cvss = candidate
selected_format = version
break

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")
ctx["base_score"] = float(raw_cvss.get("baseScore"))
ctx["base_score"] = raw_cvss["baseScore"]
ctx["base_severity"] = raw_cvss.get("baseSeverity")

vector_fields = (
"attack_complexity",
Expand Down
47 changes: 47 additions & 0 deletions src/shared/tests/test_fetchers.py
Original file line number Diff line number Diff line change
@@ -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"

53 changes: 40 additions & 13 deletions src/webview/templatetags/viewutils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -121,24 +122,50 @@ 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:
"""
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", {})
fmt = m.get("format", "")
if fmt.startswith(tuple(CVSS_PARSERS.keys())):
result = {
"vectorString": raw_cvss.get("vectorString"),
Comment thread
fricklerhandwerk marked this conversation as resolved.
"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": {},
}

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 {}


Expand Down
26 changes: 26 additions & 0 deletions src/webview/tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
},
}
24 changes: 24 additions & 0 deletions src/webview/tests/test_viewutils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
from webview.templatetags.viewutils import severity_badge


import pytest
from webview.templatetags.viewutils import severity_badge


@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"] == expected_version
assert metric["baseScore"] == expected_score
assert metric["baseSeverity"] == "CRITICAL"
assert metric["vectorString"].startswith("CVSS:")