chore: route vulnerability references to canonical URLs (#10853)

Co-authored-by: Hugo P.Brito <hugopbrito@Mac.home>
This commit is contained in:
Hugo Pereira Brito
2026-05-07 15:28:50 +01:00
committed by GitHub
co-authored by Hugo P.Brito
parent bcaa6ac488
commit 2c5d47a8cd
14 changed files with 861 additions and 20 deletions
@@ -0,0 +1,91 @@
from prowler.lib.utils.vulnerability_references import (
build_finding_reference_url,
resolve_vulnerability_reference_urls,
)
class TestBuildFindingReferenceUrl:
def test_cve_id_returns_cve_org_url(self):
assert (
build_finding_reference_url("CVE-2023-1234")
== "https://www.cve.org/CVERecord?id=CVE-2023-1234"
)
def test_lowercase_cve_id_is_normalized(self):
assert (
build_finding_reference_url("cve-2024-9999")
== "https://www.cve.org/CVERecord?id=CVE-2024-9999"
)
def test_ghsa_id_returns_github_advisory_url(self):
assert (
build_finding_reference_url("GHSA-abcd-1234-efgh")
== "https://github.com/advisories/GHSA-ABCD-1234-EFGH"
)
def test_avd_prefixed_id_strips_prefix_for_hub(self):
assert (
build_finding_reference_url("AVD-AWS-0001")
== "https://hub.prowler.com/check/AWS-0001"
)
def test_clean_trivy_id_uses_hub_directly(self):
assert (
build_finding_reference_url("AWS-0104")
== "https://hub.prowler.com/check/AWS-0104"
)
def test_kubernetes_id_uses_hub(self):
assert (
build_finding_reference_url("AVD-K8S-0001")
== "https://hub.prowler.com/check/K8S-0001"
)
def test_dockerfile_id_uses_hub(self):
assert (
build_finding_reference_url("AVD-DOCKER-0001")
== "https://hub.prowler.com/check/DOCKER-0001"
)
def test_whitespace_is_trimmed(self):
assert (
build_finding_reference_url(" AZU-0013 ")
== "https://hub.prowler.com/check/AZU-0013"
)
class TestResolveVulnerabilityReferenceUrls:
def test_cve_with_cve_org_reference_uses_it(self):
recommendation_url, additional_urls = resolve_vulnerability_reference_urls(
vulnerability_id="CVE-2023-1234",
references=[
"https://avd.aquasec.com/nvd/cve-2023-1234",
"https://www.cve.org/CVERecord?id=CVE-2023-1234",
"https://nvd.nist.gov/vuln/detail/CVE-2023-1234",
],
primary_url="https://avd.aquasec.com/nvd/cve-2023-1234",
)
assert recommendation_url == "https://www.cve.org/CVERecord?id=CVE-2023-1234"
assert additional_urls == ["https://www.cve.org/CVERecord?id=CVE-2023-1234"]
def test_cve_without_cve_org_reference_builds_url(self):
recommendation_url, additional_urls = resolve_vulnerability_reference_urls(
vulnerability_id="CVE-2023-5678",
references=["https://nvd.nist.gov/vuln/detail/CVE-2023-5678"],
)
assert recommendation_url == "https://www.cve.org/CVERecord?id=CVE-2023-5678"
assert additional_urls == ["https://www.cve.org/CVERecord?id=CVE-2023-5678"]
def test_non_cve_id_returns_filtered_references(self):
recommendation_url, additional_urls = resolve_vulnerability_reference_urls(
vulnerability_id="GHSA-abcd-1234-efgh",
references=[
"https://avd.aquasec.com/nvd/ghsa-abcd-1234-efgh",
"https://github.com/advisories/GHSA-abcd-1234-efgh",
],
)
assert recommendation_url == ""
assert additional_urls == ["https://github.com/advisories/GHSA-abcd-1234-efgh"]
+40 -1
View File
@@ -259,7 +259,13 @@ SAMPLE_TRIVY_VULNERABILITY_OUTPUT = {
"Title": "Example vulnerability",
"Description": "This is an example vulnerability",
"Severity": "high",
"PrimaryURL": "https://example.com/cve-2023-1234",
"PrimaryURL": "https://avd.aquasec.com/nvd/cve-2023-1234",
"References": [
"https://avd.aquasec.com/nvd/cve-2023-1234",
"https://nvd.nist.gov/vuln/detail/CVE-2023-1234",
"https://www.cve.org/CVERecord?id=CVE-2023-1234",
"https://security.example.com/advisories/CVE-2023-1234",
],
}
],
"Secrets": [],
@@ -268,6 +274,39 @@ SAMPLE_TRIVY_VULNERABILITY_OUTPUT = {
]
}
SAMPLE_TRIVY_VULNERABILITY_WITHOUT_CVE_ORG_REFERENCE = {
"VulnerabilityID": "CVE-2023-5678",
"Title": "Vulnerability without cve.org reference",
"Description": "This vulnerability includes references but no cve.org reference",
"Severity": "high",
"PrimaryURL": "https://avd.aquasec.com/nvd/cve-2023-5678",
"References": [
"https://avd.aquasec.com/nvd/cve-2023-5678",
"https://nvd.nist.gov/vuln/detail/CVE-2023-5678",
"https://security.example.com/advisories/CVE-2023-5678",
],
}
SAMPLE_TRIVY_VULNERABILITY_WITHOUT_REFERENCES = {
"VulnerabilityID": "CVE-2023-9012",
"Title": "Fallback CVE vulnerability",
"Description": "This vulnerability requires building the URL from VulnerabilityID",
"Severity": "medium",
"PrimaryURL": "https://avd.aquasec.com/nvd/cve-2023-9012",
}
SAMPLE_TRIVY_NON_CVE_VULNERABILITY = {
"VulnerabilityID": "GHSA-abcd-1234-efgh",
"Title": "Non-CVE vulnerability",
"Description": "This advisory has no CVE identifier",
"Severity": "high",
"PrimaryURL": "https://avd.aquasec.com/nvd/ghsa-abcd-1234-efgh",
"References": [
"https://avd.aquasec.com/nvd/ghsa-abcd-1234-efgh",
"https://github.com/advisories/GHSA-abcd-1234-efgh",
],
}
# Sample Trivy output with secrets
SAMPLE_TRIVY_SECRET_OUTPUT = {
"Results": [
+103 -3
View File
@@ -20,6 +20,9 @@ from tests.providers.iac.iac_fixtures import (
SAMPLE_KUBERNETES_CHECK,
SAMPLE_PASSED_CHECK,
SAMPLE_SKIPPED_CHECK,
SAMPLE_TRIVY_NON_CVE_VULNERABILITY,
SAMPLE_TRIVY_VULNERABILITY_WITHOUT_CVE_ORG_REFERENCE,
SAMPLE_TRIVY_VULNERABILITY_WITHOUT_REFERENCES,
SAMPLE_YAML_CHECK,
get_empty_trivy_output,
get_invalid_trivy_output,
@@ -57,13 +60,15 @@ class TestIacProvider:
assert isinstance(report, CheckReportIAC)
assert report.status == "FAIL"
# Trivy emits "AVD-AWS-0001"; Hub indexes it without the AVD- prefix.
expected_url = "https://hub.prowler.com/check/AWS-0001"
assert report.check_metadata.Provider == "iac"
assert report.check_metadata.CheckID == SAMPLE_FAILED_CHECK["ID"]
assert report.check_metadata.CheckTitle == SAMPLE_FAILED_CHECK["Title"]
assert report.check_metadata.Severity == "low"
assert report.check_metadata.RelatedUrl == SAMPLE_FAILED_CHECK.get(
"PrimaryURL", ""
)
assert report.check_metadata.Remediation.Recommendation.Url == expected_url
assert report.check_metadata.RelatedUrl == ""
assert report.check_metadata.AdditionalURLs == [expected_url]
def test_iac_provider_process_finding_passed(self):
"""Test processing a passed finding"""
@@ -79,6 +84,101 @@ class TestIacProvider:
assert report.check_metadata.CheckTitle == SAMPLE_PASSED_CHECK["Title"]
assert report.check_metadata.Severity == "low"
def test_iac_provider_process_vulnerability_prefers_cve_reference_and_filters_aqua(
self,
):
"""Test CVE findings use cve.org and exclude Aqua references."""
provider = IacProvider()
report = provider._process_finding(
{
"VulnerabilityID": "CVE-2023-1234",
"Title": "Example vulnerability",
"Description": "This is an example vulnerability",
"Severity": "high",
"PrimaryURL": "https://avd.aquasec.com/nvd/cve-2023-1234",
"References": [
"https://avd.aquasec.com/nvd/cve-2023-1234",
"https://nvd.nist.gov/vuln/detail/CVE-2023-1234",
"https://www.cve.org/CVERecord?id=CVE-2023-1234",
"https://security.example.com/advisories/CVE-2023-1234",
],
},
"package.json",
"nodejs",
)
assert (
report.check_metadata.Remediation.Recommendation.Url
== "https://www.cve.org/CVERecord?id=CVE-2023-1234"
)
assert report.check_metadata.RelatedUrl == ""
assert report.check_metadata.AdditionalURLs == [
"https://www.cve.org/CVERecord?id=CVE-2023-1234"
]
def test_iac_provider_process_vulnerability_builds_cve_org_for_nvd_reference(
self,
):
"""Test official CVE URL is built when only NVD is provided."""
provider = IacProvider()
report = provider._process_finding(
SAMPLE_TRIVY_VULNERABILITY_WITHOUT_CVE_ORG_REFERENCE,
"package.json",
"nodejs",
)
assert (
report.check_metadata.Remediation.Recommendation.Url
== "https://www.cve.org/CVERecord?id=CVE-2023-5678"
)
assert report.check_metadata.RelatedUrl == ""
assert report.check_metadata.AdditionalURLs == [
"https://www.cve.org/CVERecord?id=CVE-2023-5678"
]
def test_iac_provider_process_vulnerability_builds_cve_org_when_references_missing(
self,
):
"""Test CVE URL is built from VulnerabilityID when references are absent."""
provider = IacProvider()
report = provider._process_finding(
SAMPLE_TRIVY_VULNERABILITY_WITHOUT_REFERENCES,
"package.json",
"nodejs",
)
assert (
report.check_metadata.Remediation.Recommendation.Url
== "https://www.cve.org/CVERecord?id=CVE-2023-9012"
)
assert report.check_metadata.RelatedUrl == ""
assert report.check_metadata.AdditionalURLs == [
"https://www.cve.org/CVERecord?id=CVE-2023-9012"
]
def test_iac_provider_process_non_cve_vulnerability_falls_back_to_github_advisory(
self,
):
"""Non-CVE vulnerabilities (GHSA-…) point to GitHub Security Advisories."""
provider = IacProvider()
report = provider._process_finding(
SAMPLE_TRIVY_NON_CVE_VULNERABILITY,
"package.json",
"nodejs",
)
expected_url = (
"https://github.com/advisories/"
f"{SAMPLE_TRIVY_NON_CVE_VULNERABILITY['VulnerabilityID'].upper()}"
)
assert report.check_metadata.Remediation.Recommendation.Url == expected_url
assert report.check_metadata.RelatedUrl == ""
assert report.check_metadata.AdditionalURLs == [expected_url]
@patch("subprocess.run")
def test_iac_provider_run_scan_success(self, mock_subprocess):
"""Test successful IAC scan with Trivy"""
+50
View File
@@ -11,6 +11,12 @@ SAMPLE_VULNERABILITY_FINDING = {
"Title": "OpenSSL Buffer Overflow",
"Description": "A buffer overflow vulnerability in OpenSSL allows remote attackers to execute arbitrary code.",
"PrimaryURL": "https://avd.aquasec.com/nvd/cve-2024-1234",
"References": [
"https://avd.aquasec.com/nvd/cve-2024-1234",
"https://nvd.nist.gov/vuln/detail/CVE-2024-1234",
"https://www.cve.org/CVERecord?id=CVE-2024-1234",
"https://security.alpinelinux.org/vuln/CVE-2024-1234",
],
}
# Sample secret finding from Trivy
@@ -45,6 +51,50 @@ SAMPLE_UNKNOWN_SEVERITY_FINDING = {
"Description": "An issue with unknown severity.",
}
SAMPLE_VULNERABILITY_WITHOUT_CVE_ORG_REFERENCE = {
"VulnerabilityID": "CVE-2024-5678",
"PkgID": "libcrypto3@3.3.2-r0",
"PkgName": "libcrypto3",
"InstalledVersion": "3.3.2-r0",
"FixedVersion": "3.3.2-r1",
"Severity": "HIGH",
"Title": "OpenSSL advisory without cve.org reference",
"Description": "A vulnerability with references but no cve.org reference.",
"PrimaryURL": "https://avd.aquasec.com/nvd/cve-2024-5678",
"References": [
"https://avd.aquasec.com/nvd/cve-2024-5678",
"https://nvd.nist.gov/vuln/detail/CVE-2024-5678",
"https://security.alpinelinux.org/vuln/CVE-2024-5678",
],
}
SAMPLE_CVE_WITHOUT_REFERENCES_FINDING = {
"VulnerabilityID": "CVE-2024-9012",
"PkgID": "busybox@1.36.1-r8",
"PkgName": "busybox",
"InstalledVersion": "1.36.1-r8",
"FixedVersion": "1.36.1-r9",
"Severity": "MEDIUM",
"Title": "Busybox fallback CVE",
"Description": "A vulnerability without explicit references.",
"PrimaryURL": "https://avd.aquasec.com/nvd/cve-2024-9012",
}
SAMPLE_NON_CVE_VULNERABILITY_FINDING = {
"VulnerabilityID": "GHSA-abcd-1234-efgh",
"PkgID": "custompkg@0.0.1",
"PkgName": "custompkg",
"InstalledVersion": "0.0.1",
"Severity": "HIGH",
"Title": "Non-CVE advisory",
"Description": "An advisory without a CVE identifier.",
"PrimaryURL": "https://avd.aquasec.com/nvd/ghsa-abcd-1234-efgh",
"References": [
"https://avd.aquasec.com/nvd/ghsa-abcd-1234-efgh",
"https://github.com/advisories/GHSA-abcd-1234-efgh",
],
}
# Sample image SHA for testing (first 12 chars of a sha256 digest)
SAMPLE_IMAGE_SHA = "c1aabb73d233"
SAMPLE_IMAGE_ID = f"sha256:{SAMPLE_IMAGE_SHA}abcdef1234567890"
@@ -23,11 +23,14 @@ from prowler.providers.image.exceptions.exceptions import (
)
from prowler.providers.image.image_provider import ImageProvider
from tests.providers.image.image_fixtures import (
SAMPLE_CVE_WITHOUT_REFERENCES_FINDING,
SAMPLE_IMAGE_SHA,
SAMPLE_MISCONFIGURATION_FINDING,
SAMPLE_NON_CVE_VULNERABILITY_FINDING,
SAMPLE_SECRET_FINDING,
SAMPLE_UNKNOWN_SEVERITY_FINDING,
SAMPLE_VULNERABILITY_FINDING,
SAMPLE_VULNERABILITY_WITHOUT_CVE_ORG_REFERENCE,
get_empty_trivy_output,
get_invalid_trivy_output,
get_multi_type_trivy_output,
@@ -148,6 +151,77 @@ class TestImageProvider:
assert report.check_metadata.Categories == ["vulnerabilities"]
assert report.check_metadata.RelatedUrl == ""
def test_process_finding_vulnerability_prefers_cve_reference_and_filters_aqua(self):
"""Test CVE findings use cve.org and exclude Aqua references."""
provider = _make_provider()
report = provider._process_finding(
SAMPLE_VULNERABILITY_FINDING,
"alpine:3.18",
"alpine:3.18 (alpine 3.18.0)",
)
assert (
report.check_metadata.Remediation.Recommendation.Url
== "https://www.cve.org/CVERecord?id=CVE-2024-1234"
)
assert report.check_metadata.AdditionalURLs == [
"https://www.cve.org/CVERecord?id=CVE-2024-1234"
]
def test_process_finding_vulnerability_builds_cve_org_when_only_nvd_reference(
self,
):
"""Test official CVE URL is built when only NVD is provided."""
provider = _make_provider()
report = provider._process_finding(
SAMPLE_VULNERABILITY_WITHOUT_CVE_ORG_REFERENCE,
"alpine:3.18",
"alpine:3.18 (alpine 3.18.0)",
)
assert (
report.check_metadata.Remediation.Recommendation.Url
== "https://www.cve.org/CVERecord?id=CVE-2024-5678"
)
assert report.check_metadata.AdditionalURLs == [
"https://www.cve.org/CVERecord?id=CVE-2024-5678"
]
def test_process_finding_vulnerability_builds_cve_org_when_references_missing(self):
"""Test CVE URL is built from VulnerabilityID when references are absent."""
provider = _make_provider()
report = provider._process_finding(
SAMPLE_CVE_WITHOUT_REFERENCES_FINDING,
"alpine:3.18",
"alpine:3.18 (alpine 3.18.0)",
)
assert (
report.check_metadata.Remediation.Recommendation.Url
== "https://www.cve.org/CVERecord?id=CVE-2024-9012"
)
assert report.check_metadata.AdditionalURLs == [
"https://www.cve.org/CVERecord?id=CVE-2024-9012"
]
def test_process_finding_non_cve_vulnerability_does_not_fallback_to_aqua(self):
"""Test non-CVE vulnerabilities do not keep Aqua links."""
provider = _make_provider()
report = provider._process_finding(
SAMPLE_NON_CVE_VULNERABILITY_FINDING,
"alpine:3.18",
"alpine:3.18 (alpine 3.18.0)",
)
assert report.check_metadata.Remediation.Recommendation.Url == ""
assert report.check_metadata.AdditionalURLs == [
"https://github.com/advisories/GHSA-abcd-1234-efgh"
]
def test_process_finding_secret(self):
"""Test processing a secret finding (identified by RuleID)."""
provider = _make_provider()