Compare commits

...

17 Commits

Author SHA1 Message Date
Hugo P.Brito 4dbb7c7bb7 fix(ui): restore View Resource action in findings drawer
Reapplies the View Resource link that was inadvertently dropped while
removing this PR's overlap with #10847. That feature is already on master
and removing it here would have regressed the findings drawer.

Restores buildResourceDetailHref, the resourceDetailHref binding, the
JSX action below the resource actions menu, and the original positive
assertion in the drawer test.
2026-05-06 09:34:20 +01:00
Hugo P.Brito 64df3a22c0 chore: merge master into PR 10853 2026-05-06 09:09:57 +01:00
Hugo P.Brito e6fa47be67 fix(ui): refine recommendation link handling
- Move recommendation URL labels to shared UI utilities

- Remove unrelated resource navigation from the drawer diff

- Keep URL-only remediation cards labeled and update tests
2026-05-04 09:32:15 +01:00
Hugo P.Brito 545c14d08c Merge remote-tracking branch 'origin/master' into fix/ui-cve-fix-available-links
# Conflicts:
#	ui/CHANGELOG.md
2026-04-30 12:26:35 +01:00
Hugo P.Brito de0ddf5c48 docs: move CVE link entries to next unreleased version
- Move SDK entry from 5.25.0 to 5.26.0 (Prowler UNRELEASED)
- Add a 1.26.0 (Prowler UNRELEASED) section in the UI changelog and move the entry there
2026-04-30 12:16:03 +01:00
Hugo P.Brito 5310b9e402 fix(ui): label remediation action by destination
- Render "View in Prowler Hub" for hub.prowler.com URLs and "View Advisory" for GitHub Security Advisory URLs alongside the existing "View CVE" action
- Fall back to a generic "View Reference" for other destinations
- Cover the new label with a unit test; refresh changelog entry
2026-04-29 16:11:50 +01:00
Hugo P.Brito ab6f36455f fix(sdk): route Trivy findings to real CVE, Hub and Advisory URLs
- Add `build_finding_reference_url` mapping a finding ID to its canonical reference (`cve.org`, `github.com/advisories`, or `hub.prowler.com/check`)
- IAC provider falls back to the helper when no canonical CVE URL is resolved, so misconfigs and non-CVE vulns get a working remediation link instead of an Aqua URL
- Strip leading `AVD-` so Prowler Hub URLs resolve, since Hub indexes Trivy rules without the prefix
- Cover the helper and IAC behavior with unit tests; refresh changelog entry
2026-04-29 16:09:49 +01:00
Hugo P.Brito 67472fa378 chore: merge master into CVE link PR 2026-04-29 11:33:09 +01:00
Hugo P.Brito a22db1172a docs(sdk): add CVE link changelog entry 2026-04-29 11:31:05 +01:00
Hugo P.Brito 9b50dbceb3 fix(ui): label remediation links by destination
- Use recommendation URLs as the single CTA source

- Keep Prowler Hub and CVE labels distinct

- Assert official CVE references are rendered without Aqua URLs
2026-04-29 11:30:35 +01:00
Hugo P.Brito 9645544552 fix(sdk): use official CVE URLs for Trivy findings
- Resolve CVE recommendations from Trivy references

- Remove Aqua advisory URLs from provider metadata

- Preserve Prowler Hub remediation links for IaC checks

- Cover CVE fallback and non-CVE advisory cases
2026-04-29 11:29:01 +01:00
Hugo P.Brito e970a26ad8 Merge remote-tracking branch 'origin/master' into pr-10853-conflicts
# Conflicts:
#	ui/components/findings/table/resource-detail-drawer/resource-detail-drawer-content.tsx
2026-04-27 11:06:53 +01:00
Hugo P.Brito 5da90f9ea3 refactor(ui): drop status-extended linkification, keep CVE action only 2026-04-23 11:29:27 +01:00
Hugo P.Brito e06a682cef fix(ui): show CVE action in remediation drawer
- Use external CVE references when a finding has no Hub recommendation
- Keep the remediation CTA slot as View in Prowler Hub or View CVE
- Cover the CVE drawer behavior with resource detail tests
2026-04-23 11:12:11 +01:00
Hugo P.Brito dd45eeea48 Merge remote-tracking branch 'origin/master' into fix/ui-cve-fix-available-links 2026-04-23 11:08:56 +01:00
Hugo P.Brito ca7bba28c4 docs(ui): link changelog entry to PR 2026-04-22 15:28:09 +01:00
Hugo P.Brito 4205ebda04 fix(ui): link CVE fix-available versions
- Link fix available versions in finding details for external CVE advisories
- Keep Prowler Hub-backed checks on the existing remediation path
- Cover the drawer behavior with focused UI tests
2026-04-22 15:26:03 +01:00
14 changed files with 884 additions and 20 deletions
+8
View File
@@ -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]
+21 -3
View File
@@ -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": "",
+20 -4
View File
@@ -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"]
+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": [
+112 -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 == 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"""
+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()
+8
View File
@@ -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
@@ -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
// ---------------------------------------------------------------------------
@@ -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>
+41
View File
@@ -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");
});
});
+56
View File
@@ -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
);
}