mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-05-06 08:47:18 +00:00
Compare commits
17 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 4dbb7c7bb7 | |||
| 64df3a22c0 | |||
| e6fa47be67 | |||
| 545c14d08c | |||
| de0ddf5c48 | |||
| 5310b9e402 | |||
| ab6f36455f | |||
| 67472fa378 | |||
| a22db1172a | |||
| 9b50dbceb3 | |||
| 9645544552 | |||
| e970a26ad8 | |||
| 5da90f9ea3 | |||
| e06a682cef | |||
| dd45eeea48 | |||
| ca7bba28c4 | |||
| 4205ebda04 |
@@ -33,6 +33,14 @@ All notable changes to the **Prowler SDK** are documented in this file.
|
||||
|
||||
---
|
||||
|
||||
## [5.25.3] (Prowler UNRELEASED)
|
||||
|
||||
### 🐞 Fixed
|
||||
|
||||
- Container image CVE findings and IaC findings now use official CVE, Prowler Hub, or GitHub Security Advisory URLs instead of Aqua advisory URLs in remediation and references; Trivy rule IDs map to Prowler Hub without the `AVD-` prefix so links resolve [(#10853)](https://github.com/prowler-cloud/prowler/pull/10853)
|
||||
|
||||
---
|
||||
|
||||
## [5.25.2] (Prowler v5.25.2)
|
||||
|
||||
### 🐞 Fixed
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
import re
|
||||
from urllib.parse import parse_qs, urlparse
|
||||
|
||||
AQUA_REFERENCE_HOST = "avd.aquasec.com"
|
||||
GITHUB_ADVISORY_URL = "https://github.com/advisories/{advisory_id}"
|
||||
PROWLER_HUB_CHECK_URL = "https://hub.prowler.com/check/{check_id}"
|
||||
_CVE_ID_PATTERN = re.compile(r"^CVE-\d{4}-\d+$", re.IGNORECASE)
|
||||
_GHSA_ID_PATTERN = re.compile(r"^GHSA(?:-[a-z0-9]{4}){3}$", re.IGNORECASE)
|
||||
|
||||
|
||||
def _dedupe_preserve_order(urls: list[str]) -> list[str]:
|
||||
seen: set[str] = set()
|
||||
ordered_urls: list[str] = []
|
||||
|
||||
for url in urls:
|
||||
if not url or not url.strip():
|
||||
continue
|
||||
|
||||
normalized_url = url.strip()
|
||||
if normalized_url in seen:
|
||||
continue
|
||||
|
||||
seen.add(normalized_url)
|
||||
ordered_urls.append(normalized_url)
|
||||
|
||||
return ordered_urls
|
||||
|
||||
|
||||
def _is_aqua_reference(url: str) -> bool:
|
||||
return AQUA_REFERENCE_HOST in urlparse(url).netloc.lower()
|
||||
|
||||
|
||||
def _build_cve_org_url(vulnerability_id: str) -> str:
|
||||
return f"https://www.cve.org/CVERecord?id={vulnerability_id.upper()}"
|
||||
|
||||
|
||||
def build_finding_reference_url(finding_id: str) -> str:
|
||||
"""Map a Trivy finding ID to a stable, real reference URL.
|
||||
|
||||
- CVE-XXXX-NNNN → cve.org record
|
||||
- GHSA-… → github.com/advisories
|
||||
- everything else → hub.prowler.com/check/<id>, stripping a leading
|
||||
"AVD-" prefix because Prowler Hub indexes Trivy rules by the
|
||||
non-prefixed ID (e.g., "AWS-0001" not "AVD-AWS-0001").
|
||||
"""
|
||||
normalized = finding_id.strip().upper()
|
||||
if _CVE_ID_PATTERN.match(normalized):
|
||||
return _build_cve_org_url(normalized)
|
||||
if _GHSA_ID_PATTERN.match(normalized):
|
||||
return GITHUB_ADVISORY_URL.format(advisory_id=normalized)
|
||||
hub_id = normalized[4:] if normalized.startswith("AVD-") else normalized
|
||||
return PROWLER_HUB_CHECK_URL.format(check_id=hub_id)
|
||||
|
||||
|
||||
def _is_cve_org_url(url: str, vulnerability_id: str) -> bool:
|
||||
parsed_url = urlparse(url)
|
||||
if parsed_url.netloc.lower() != "www.cve.org":
|
||||
return False
|
||||
|
||||
query_value = parse_qs(parsed_url.query).get("id", [""])[0]
|
||||
return query_value.upper() == vulnerability_id.upper()
|
||||
|
||||
|
||||
def resolve_vulnerability_reference_urls(
|
||||
vulnerability_id: str,
|
||||
references: list[str] | None = None,
|
||||
primary_url: str = "",
|
||||
) -> tuple[str, list[str]]:
|
||||
"""Resolve non-Aqua vulnerability URLs, prioritizing official CVE destinations."""
|
||||
|
||||
candidate_urls = list(references or [])
|
||||
if primary_url and primary_url not in candidate_urls:
|
||||
candidate_urls.append(primary_url)
|
||||
|
||||
filtered_urls = _dedupe_preserve_order(
|
||||
[url for url in candidate_urls if not _is_aqua_reference(url)]
|
||||
)
|
||||
|
||||
if not _CVE_ID_PATTERN.match(vulnerability_id):
|
||||
return "", filtered_urls
|
||||
|
||||
cve_org_urls = [
|
||||
url for url in filtered_urls if _is_cve_org_url(url, vulnerability_id)
|
||||
]
|
||||
|
||||
recommendation_url = (
|
||||
cve_org_urls[0] if cve_org_urls else _build_cve_org_url(vulnerability_id)
|
||||
)
|
||||
|
||||
return recommendation_url, [recommendation_url]
|
||||
@@ -18,6 +18,10 @@ from prowler.config.config import (
|
||||
from prowler.lib.check.models import CheckReportIAC
|
||||
from prowler.lib.logger import logger
|
||||
from prowler.lib.utils.utils import print_boxes
|
||||
from prowler.lib.utils.vulnerability_references import (
|
||||
build_finding_reference_url,
|
||||
resolve_vulnerability_reference_urls,
|
||||
)
|
||||
from prowler.providers.common.models import Audit_Metadata, Connection
|
||||
from prowler.providers.common.provider import Provider
|
||||
|
||||
@@ -189,14 +193,28 @@ class IacProvider(Provider):
|
||||
finding_id = finding["VulnerabilityID"]
|
||||
finding_description = finding["Description"]
|
||||
finding_status = finding.get("Status", "FAIL")
|
||||
recommendation_url, additional_urls = (
|
||||
resolve_vulnerability_reference_urls(
|
||||
vulnerability_id=finding_id,
|
||||
references=finding.get("References"),
|
||||
primary_url=finding.get("PrimaryURL", ""),
|
||||
)
|
||||
)
|
||||
if not recommendation_url:
|
||||
recommendation_url = build_finding_reference_url(finding_id)
|
||||
additional_urls = [recommendation_url]
|
||||
elif "RuleID" in finding:
|
||||
finding_id = finding["RuleID"]
|
||||
finding_description = finding["Title"]
|
||||
finding_status = finding.get("Status", "FAIL")
|
||||
recommendation_url = build_finding_reference_url(finding_id)
|
||||
additional_urls = [recommendation_url]
|
||||
else:
|
||||
finding_id = finding["ID"]
|
||||
finding_description = finding["Description"]
|
||||
finding_status = finding["Status"]
|
||||
recommendation_url = build_finding_reference_url(finding_id)
|
||||
additional_urls = [recommendation_url]
|
||||
|
||||
metadata_dict = {
|
||||
"Provider": "iac",
|
||||
@@ -210,7 +228,7 @@ class IacProvider(Provider):
|
||||
"ResourceType": "iac",
|
||||
"Description": finding_description,
|
||||
"Risk": "This provider has not defined a risk for this check.",
|
||||
"RelatedUrl": finding.get("PrimaryURL", ""),
|
||||
"RelatedUrl": recommendation_url,
|
||||
"Remediation": {
|
||||
"Code": {
|
||||
"NativeIaC": "",
|
||||
@@ -220,11 +238,11 @@ class IacProvider(Provider):
|
||||
},
|
||||
"Recommendation": {
|
||||
"Text": finding.get("Resolution", ""),
|
||||
"Url": finding.get("PrimaryURL", ""),
|
||||
"Url": recommendation_url,
|
||||
},
|
||||
},
|
||||
"Categories": [],
|
||||
"AdditionalURLs": [],
|
||||
"AdditionalURLs": additional_urls,
|
||||
"DependsOn": [],
|
||||
"RelatedTo": [],
|
||||
"Notes": "",
|
||||
|
||||
@@ -18,6 +18,9 @@ from prowler.config.config import (
|
||||
from prowler.lib.check.models import CheckReportImage
|
||||
from prowler.lib.logger import logger
|
||||
from prowler.lib.utils.utils import print_boxes
|
||||
from prowler.lib.utils.vulnerability_references import (
|
||||
resolve_vulnerability_reference_urls,
|
||||
)
|
||||
from prowler.providers.common.models import Audit_Metadata, Connection
|
||||
from prowler.providers.common.provider import Provider
|
||||
from prowler.providers.image.exceptions.exceptions import (
|
||||
@@ -395,6 +398,8 @@ class ImageProvider(Provider):
|
||||
"""
|
||||
try:
|
||||
# Determine finding ID and category based on type
|
||||
recommendation_url = ""
|
||||
additional_urls: list[str] = []
|
||||
if "VulnerabilityID" in finding:
|
||||
finding_id = finding["VulnerabilityID"]
|
||||
finding_description = finding.get(
|
||||
@@ -402,17 +407,30 @@ class ImageProvider(Provider):
|
||||
)
|
||||
finding_status = "FAIL"
|
||||
finding_categories = ["vulnerabilities"]
|
||||
recommendation_url, additional_urls = (
|
||||
resolve_vulnerability_reference_urls(
|
||||
vulnerability_id=finding_id,
|
||||
references=finding.get("References"),
|
||||
primary_url=finding.get("PrimaryURL", ""),
|
||||
)
|
||||
)
|
||||
elif "RuleID" in finding:
|
||||
# Secret finding
|
||||
finding_id = finding["RuleID"]
|
||||
finding_description = finding.get("Title", "Secret detected")
|
||||
finding_status = "FAIL"
|
||||
finding_categories = ["secrets"]
|
||||
additional_urls = (
|
||||
[url] if (url := finding.get("PrimaryURL", "")) else []
|
||||
)
|
||||
else:
|
||||
finding_id = finding.get("ID", "UNKNOWN")
|
||||
finding_description = finding.get("Description", "")
|
||||
finding_status = finding.get("Status", "FAIL")
|
||||
finding_categories = []
|
||||
additional_urls = (
|
||||
[url] if (url := finding.get("PrimaryURL", "")) else []
|
||||
)
|
||||
|
||||
# Build remediation text for vulnerabilities
|
||||
remediation_text = ""
|
||||
@@ -451,13 +469,11 @@ class ImageProvider(Provider):
|
||||
},
|
||||
"Recommendation": {
|
||||
"Text": remediation_text,
|
||||
"Url": "",
|
||||
"Url": recommendation_url,
|
||||
},
|
||||
},
|
||||
"Categories": finding_categories,
|
||||
"AdditionalURLs": (
|
||||
[url] if (url := finding.get("PrimaryURL", "")) else []
|
||||
),
|
||||
"AdditionalURLs": additional_urls,
|
||||
"DependsOn": [],
|
||||
"RelatedTo": [],
|
||||
"Notes": "",
|
||||
|
||||
@@ -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"]
|
||||
@@ -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": [
|
||||
|
||||
@@ -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 == expected_url
|
||||
assert report.check_metadata.AdditionalURLs == [expected_url]
|
||||
|
||||
def test_iac_provider_process_finding_passed(self):
|
||||
"""Test processing a passed finding"""
|
||||
@@ -79,6 +84,110 @@ 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
|
||||
== "https://www.cve.org/CVERecord?id=CVE-2023-1234"
|
||||
)
|
||||
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
|
||||
== "https://www.cve.org/CVERecord?id=CVE-2023-5678"
|
||||
)
|
||||
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
|
||||
== "https://www.cve.org/CVERecord?id=CVE-2023-9012"
|
||||
)
|
||||
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 == expected_url
|
||||
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"""
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -10,6 +10,14 @@ All notable changes to the **Prowler UI** are documented in this file.
|
||||
|
||||
---
|
||||
|
||||
## [1.25.3] (Prowler UNRELEASED)
|
||||
|
||||
### 🐞 Fixed
|
||||
|
||||
- Finding detail drawer now labels remediation actions from finding-level recommendation URLs by destination: "View CVE", "View in Prowler Hub", "View Advisory", or "View Reference", while keeping URL-only remediation cards labeled [(#10853)](https://github.com/prowler-cloud/prowler/pull/10853)
|
||||
|
||||
---
|
||||
|
||||
## [1.25.2] (Prowler v5.25.2)
|
||||
|
||||
### 🔄 Changed
|
||||
|
||||
+242
@@ -651,6 +651,248 @@ describe("ResourceDetailDrawerContent — Fix 2: Remediation heading labels", ()
|
||||
});
|
||||
});
|
||||
|
||||
describe("ResourceDetailDrawerContent — CVE recommendation button", () => {
|
||||
const statusExtendedWithFixVersions =
|
||||
"framework.security.spring-security-web@5.8.7 (fix available: 5.7.13, 5.8.15, 6.2.7, 6.0.13, 6.1.11, 6.3.4)";
|
||||
const externalCveUrl = "https://www.cve.org/CVERecord?id=CVE-2026-12345";
|
||||
|
||||
it("should render a View CVE button when the recommendation URL points to an external CVE advisory and keep status extended as plain text", () => {
|
||||
const cveCheckMeta: CheckMeta = {
|
||||
...mockCheckMeta,
|
||||
remediation: {
|
||||
...mockCheckMeta.remediation,
|
||||
recommendation: {
|
||||
text: "Review the advisory",
|
||||
url: externalCveUrl,
|
||||
},
|
||||
},
|
||||
};
|
||||
const cveFinding: ResourceDrawerFinding = {
|
||||
...mockFinding,
|
||||
statusExtended: statusExtendedWithFixVersions,
|
||||
remediation: {
|
||||
...mockFinding.remediation,
|
||||
recommendation: {
|
||||
text: "Review the advisory",
|
||||
url: externalCveUrl,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
render(
|
||||
<ResourceDetailDrawerContent
|
||||
isLoading={false}
|
||||
isNavigating={false}
|
||||
checkMeta={cveCheckMeta}
|
||||
currentIndex={0}
|
||||
totalResources={1}
|
||||
currentFinding={cveFinding}
|
||||
otherFindings={[]}
|
||||
onNavigatePrev={vi.fn()}
|
||||
onNavigateNext={vi.fn()}
|
||||
onMuteComplete={vi.fn()}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(screen.getByRole("link", { name: "View CVE" })).toHaveAttribute(
|
||||
"href",
|
||||
externalCveUrl,
|
||||
);
|
||||
expect(screen.getByText(statusExtendedWithFixVersions)).toBeInTheDocument();
|
||||
expect(
|
||||
screen.queryByRole("link", { name: "View in Prowler Hub" }),
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should show View in Prowler Hub when the recommendation URL points to Prowler Hub", () => {
|
||||
const hubCheckMeta: CheckMeta = {
|
||||
...mockCheckMeta,
|
||||
remediation: {
|
||||
...mockCheckMeta.remediation,
|
||||
recommendation: {
|
||||
text: "Open the check in Hub",
|
||||
url: "https://hub.prowler.com/check/image_vulnerability",
|
||||
},
|
||||
},
|
||||
};
|
||||
const hubFinding: ResourceDrawerFinding = {
|
||||
...mockFinding,
|
||||
statusExtended: statusExtendedWithFixVersions,
|
||||
remediation: {
|
||||
...mockFinding.remediation,
|
||||
recommendation: {
|
||||
text: "Open the check in Hub",
|
||||
url: "https://hub.prowler.com/check/image_vulnerability",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
render(
|
||||
<ResourceDetailDrawerContent
|
||||
isLoading={false}
|
||||
isNavigating={false}
|
||||
checkMeta={hubCheckMeta}
|
||||
currentIndex={0}
|
||||
totalResources={1}
|
||||
currentFinding={hubFinding}
|
||||
otherFindings={[]}
|
||||
onNavigatePrev={vi.fn()}
|
||||
onNavigateNext={vi.fn()}
|
||||
onMuteComplete={vi.fn()}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(screen.getByText(statusExtendedWithFixVersions)).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByRole("link", { name: "View in Prowler Hub" }),
|
||||
).toHaveAttribute(
|
||||
"href",
|
||||
"https://hub.prowler.com/check/image_vulnerability",
|
||||
);
|
||||
});
|
||||
|
||||
it("should render the official CVE reference", () => {
|
||||
const cveCheckMeta: CheckMeta = {
|
||||
...mockCheckMeta,
|
||||
remediation: {
|
||||
...mockCheckMeta.remediation,
|
||||
recommendation: {
|
||||
text: "Review the advisory",
|
||||
url: externalCveUrl,
|
||||
},
|
||||
},
|
||||
additionalUrls: [externalCveUrl],
|
||||
};
|
||||
const cveFinding: ResourceDrawerFinding = {
|
||||
...mockFinding,
|
||||
statusExtended: statusExtendedWithFixVersions,
|
||||
remediation: {
|
||||
...mockFinding.remediation,
|
||||
recommendation: {
|
||||
text: "Review the advisory",
|
||||
url: externalCveUrl,
|
||||
},
|
||||
},
|
||||
additionalUrls: [externalCveUrl],
|
||||
};
|
||||
|
||||
render(
|
||||
<ResourceDetailDrawerContent
|
||||
isLoading={false}
|
||||
isNavigating={false}
|
||||
checkMeta={cveCheckMeta}
|
||||
currentIndex={0}
|
||||
totalResources={1}
|
||||
currentFinding={cveFinding}
|
||||
otherFindings={[]}
|
||||
onNavigatePrev={vi.fn()}
|
||||
onNavigateNext={vi.fn()}
|
||||
onMuteComplete={vi.fn()}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(screen.getByRole("link", { name: "View CVE" })).toHaveAttribute(
|
||||
"href",
|
||||
externalCveUrl,
|
||||
);
|
||||
expect(screen.getByRole("link", { name: externalCveUrl })).toHaveAttribute(
|
||||
"href",
|
||||
externalCveUrl,
|
||||
);
|
||||
});
|
||||
|
||||
it("should render View Advisory when the recommendation URL points to GitHub Security Advisories", () => {
|
||||
const advisoryUrl = "https://github.com/advisories/GHSA-abcd-1234-efgh";
|
||||
const advisoryCheckMeta: CheckMeta = {
|
||||
...mockCheckMeta,
|
||||
remediation: {
|
||||
...mockCheckMeta.remediation,
|
||||
recommendation: {
|
||||
text: "Review the advisory",
|
||||
url: advisoryUrl,
|
||||
},
|
||||
},
|
||||
};
|
||||
const advisoryFinding: ResourceDrawerFinding = {
|
||||
...mockFinding,
|
||||
remediation: {
|
||||
...mockFinding.remediation,
|
||||
recommendation: {
|
||||
text: "Review the advisory",
|
||||
url: advisoryUrl,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
render(
|
||||
<ResourceDetailDrawerContent
|
||||
isLoading={false}
|
||||
isNavigating={false}
|
||||
checkMeta={advisoryCheckMeta}
|
||||
currentIndex={0}
|
||||
totalResources={1}
|
||||
currentFinding={advisoryFinding}
|
||||
otherFindings={[]}
|
||||
onNavigatePrev={vi.fn()}
|
||||
onNavigateNext={vi.fn()}
|
||||
onMuteComplete={vi.fn()}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(screen.getByRole("link", { name: "View Advisory" })).toHaveAttribute(
|
||||
"href",
|
||||
advisoryUrl,
|
||||
);
|
||||
expect(
|
||||
screen.queryByRole("link", { name: "View CVE" }),
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should render a remediation label when the only remediation content is a recommendation link", () => {
|
||||
const cveCheckMeta: CheckMeta = {
|
||||
...mockCheckMeta,
|
||||
remediation: {
|
||||
...mockCheckMeta.remediation,
|
||||
recommendation: {
|
||||
text: "",
|
||||
url: externalCveUrl,
|
||||
},
|
||||
},
|
||||
};
|
||||
const cveFinding: ResourceDrawerFinding = {
|
||||
...mockFinding,
|
||||
remediation: {
|
||||
...mockFinding.remediation,
|
||||
recommendation: {
|
||||
text: "",
|
||||
url: externalCveUrl,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
render(
|
||||
<ResourceDetailDrawerContent
|
||||
isLoading={false}
|
||||
isNavigating={false}
|
||||
checkMeta={cveCheckMeta}
|
||||
currentIndex={0}
|
||||
totalResources={1}
|
||||
currentFinding={cveFinding}
|
||||
otherFindings={[]}
|
||||
onNavigatePrev={vi.fn()}
|
||||
onNavigateNext={vi.fn()}
|
||||
onMuteComplete={vi.fn()}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(screen.getByText("Remediation:")).toBeInTheDocument();
|
||||
expect(screen.getByRole("link", { name: "View CVE" })).toHaveAttribute(
|
||||
"href",
|
||||
externalCveUrl,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Fix 5 & 6: Risk section has danger styling, sections have separators and bigger headings
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
+31
-9
@@ -70,6 +70,7 @@ import { getFailingForLabel } from "@/lib/date-utils";
|
||||
import { formatDuration } from "@/lib/date-utils";
|
||||
import { getRegionFlag } from "@/lib/region-flags";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { getRecommendationLinkLabel } from "@/lib/vulnerability-references";
|
||||
import type { ComplianceOverviewData } from "@/types/compliance";
|
||||
import type { FindingResourceRow } from "@/types/findings-table";
|
||||
|
||||
@@ -87,6 +88,10 @@ function stripCodeFences(code: string): string {
|
||||
.trim();
|
||||
}
|
||||
|
||||
function isNonEmptyString(value: string | null | undefined): value is string {
|
||||
return typeof value === "string" && value.trim().length > 0;
|
||||
}
|
||||
|
||||
function resolveNativeIacConfig(providerType: string | undefined): {
|
||||
label: string;
|
||||
language: QueryEditorLanguage;
|
||||
@@ -425,6 +430,19 @@ export function ResourceDetailDrawerContent({
|
||||
const resourceDetailHref = f?.resourceId
|
||||
? buildResourceDetailHref(f.resourceId)
|
||||
: null;
|
||||
const findingRecommendationUrl = f?.remediation.recommendation.url;
|
||||
const checkRecommendationUrl = checkMeta.remediation.recommendation.url;
|
||||
const recommendationUrl = isNonEmptyString(findingRecommendationUrl)
|
||||
? findingRecommendationUrl
|
||||
: isNonEmptyString(checkRecommendationUrl)
|
||||
? checkRecommendationUrl
|
||||
: null;
|
||||
const recommendationLink = recommendationUrl
|
||||
? {
|
||||
href: recommendationUrl,
|
||||
label: getRecommendationLinkLabel(recommendationUrl),
|
||||
}
|
||||
: null;
|
||||
const overviewStatusExtended = f?.statusExtended;
|
||||
const showOverviewStatusExtended = Boolean(overviewStatusExtended);
|
||||
|
||||
@@ -852,28 +870,32 @@ export function ResourceDetailDrawerContent({
|
||||
|
||||
{/* Card 2: Remediation + Commands */}
|
||||
{(checkMeta.remediation.recommendation.text ||
|
||||
recommendationLink ||
|
||||
checkMeta.remediation.code.cli ||
|
||||
checkMeta.remediation.code.terraform ||
|
||||
checkMeta.remediation.code.nativeiac) && (
|
||||
<Card variant="inner">
|
||||
{checkMeta.remediation.recommendation.text && (
|
||||
{(checkMeta.remediation.recommendation.text ||
|
||||
recommendationLink) && (
|
||||
<div className="flex flex-col gap-1">
|
||||
<span className="text-text-neutral-secondary text-xs">
|
||||
Remediation:
|
||||
</span>
|
||||
<div className="flex items-start gap-3">
|
||||
<div className="text-text-neutral-primary flex-1 text-sm">
|
||||
<MarkdownContainer>
|
||||
{checkMeta.remediation.recommendation.text}
|
||||
</MarkdownContainer>
|
||||
</div>
|
||||
{checkMeta.remediation.recommendation.url && (
|
||||
{checkMeta.remediation.recommendation.text && (
|
||||
<div className="text-text-neutral-primary flex-1 text-sm">
|
||||
<MarkdownContainer>
|
||||
{checkMeta.remediation.recommendation.text}
|
||||
</MarkdownContainer>
|
||||
</div>
|
||||
)}
|
||||
{recommendationLink && (
|
||||
<CustomLink
|
||||
href={checkMeta.remediation.recommendation.url}
|
||||
href={recommendationLink.href}
|
||||
size="sm"
|
||||
className="shrink-0"
|
||||
>
|
||||
View in Prowler Hub
|
||||
{recommendationLink.label}
|
||||
</CustomLink>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { getRecommendationLinkLabel } from "./vulnerability-references";
|
||||
|
||||
describe("getRecommendationLinkLabel", () => {
|
||||
it("returns View CVE for known CVE hosts", () => {
|
||||
expect(
|
||||
getRecommendationLinkLabel("https://www.cve.org/CVERecord?id=CVE-2026-1"),
|
||||
).toBe("View CVE");
|
||||
expect(
|
||||
getRecommendationLinkLabel("https://cve.org/CVERecord?id=CVE-2026-1"),
|
||||
).toBe("View CVE");
|
||||
expect(
|
||||
getRecommendationLinkLabel("https://cve.mitre.org/cgi-bin/cvename.cgi"),
|
||||
).toBe("View CVE");
|
||||
});
|
||||
|
||||
it("returns View in Prowler Hub only for the exact Hub hostname", () => {
|
||||
expect(
|
||||
getRecommendationLinkLabel("https://hub.prowler.com/check/example"),
|
||||
).toBe("View in Prowler Hub");
|
||||
expect(
|
||||
getRecommendationLinkLabel("https://hub.prowler.com.evil.com/check"),
|
||||
).toBe("View Reference");
|
||||
});
|
||||
|
||||
it("returns View Advisory for GitHub Security Advisory URLs", () => {
|
||||
expect(
|
||||
getRecommendationLinkLabel(
|
||||
"https://github.com/advisories/GHSA-abcd-1234-efgh",
|
||||
),
|
||||
).toBe("View Advisory");
|
||||
});
|
||||
|
||||
it("returns View Reference for unknown or malformed URLs", () => {
|
||||
expect(getRecommendationLinkLabel("https://example.com/advisory")).toBe(
|
||||
"View Reference",
|
||||
);
|
||||
expect(getRecommendationLinkLabel("not-a-url")).toBe("View Reference");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,56 @@
|
||||
const RECOMMENDATION_LINK_LABEL = {
|
||||
CVE: "View CVE",
|
||||
HUB: "View in Prowler Hub",
|
||||
ADVISORY: "View Advisory",
|
||||
REFERENCE: "View Reference",
|
||||
} as const;
|
||||
|
||||
export type RecommendationLinkLabel =
|
||||
(typeof RECOMMENDATION_LINK_LABEL)[keyof typeof RECOMMENDATION_LINK_LABEL];
|
||||
|
||||
const CVE_HOSTS = new Set(["www.cve.org", "cve.org", "cve.mitre.org"]);
|
||||
|
||||
interface RecommendationLinkRule {
|
||||
label: RecommendationLinkLabel;
|
||||
matches: (url: URL) => boolean;
|
||||
}
|
||||
|
||||
const RECOMMENDATION_LINK_RULES: RecommendationLinkRule[] = [
|
||||
{
|
||||
label: RECOMMENDATION_LINK_LABEL.CVE,
|
||||
matches: (url) => CVE_HOSTS.has(url.hostname.toLowerCase()),
|
||||
},
|
||||
{
|
||||
label: RECOMMENDATION_LINK_LABEL.HUB,
|
||||
matches: (url) => url.hostname.toLowerCase() === "hub.prowler.com",
|
||||
},
|
||||
{
|
||||
label: RECOMMENDATION_LINK_LABEL.ADVISORY,
|
||||
matches: (url) =>
|
||||
url.hostname.toLowerCase() === "github.com" &&
|
||||
url.pathname.startsWith("/advisories/"),
|
||||
},
|
||||
];
|
||||
|
||||
function safeParseUrl(url: string): URL | null {
|
||||
try {
|
||||
return new URL(url);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export function getRecommendationLinkLabel(
|
||||
url: string,
|
||||
): RecommendationLinkLabel {
|
||||
const parsedUrl = safeParseUrl(url);
|
||||
|
||||
if (!parsedUrl) {
|
||||
return RECOMMENDATION_LINK_LABEL.REFERENCE;
|
||||
}
|
||||
|
||||
return (
|
||||
RECOMMENDATION_LINK_RULES.find((rule) => rule.matches(parsedUrl))?.label ??
|
||||
RECOMMENDATION_LINK_LABEL.REFERENCE
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user