mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-03-21 18:58:04 +00:00
fix(sdk): skip strict CheckMetadata validators for external tool providers (#10363)
This commit is contained in:
@@ -31,6 +31,7 @@ All notable changes to the **Prowler SDK** are documented in this file.
|
||||
|
||||
- Route53 dangling IP check false positive when using `--region` flag [(#9952)](https://github.com/prowler-cloud/prowler/pull/9952)
|
||||
- RBI compliance framework support on Prowler Dashboard for the Azure provider [(#10360)](https://github.com/prowler-cloud/prowler/pull/10360)
|
||||
- CheckMetadata strict validators rejecting valid external tool provider data (image, iac, llm) [(#10363)](https://github.com/prowler-cloud/prowler/pull/10363)
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -245,7 +245,7 @@ class CheckMetadata(BaseModel):
|
||||
Compliance: Optional[list[Any]] = Field(default_factory=list)
|
||||
|
||||
@validator("Categories", each_item=True, pre=True, always=True)
|
||||
def valid_category(value):
|
||||
def valid_category(cls, value, values):
|
||||
if not isinstance(value, str):
|
||||
raise ValueError("Categories must be a list of strings")
|
||||
value_lower = value.lower()
|
||||
@@ -253,7 +253,10 @@ class CheckMetadata(BaseModel):
|
||||
raise ValueError(
|
||||
f"Invalid category: {value}. Categories can only contain lowercase letters, numbers and hyphen '-'"
|
||||
)
|
||||
if value_lower not in VALID_CATEGORIES:
|
||||
if (
|
||||
value_lower not in VALID_CATEGORIES
|
||||
and values.get("Provider") not in EXTERNAL_TOOL_PROVIDERS
|
||||
):
|
||||
raise ValueError(
|
||||
f"Invalid category: '{value_lower}'. Must be one of: {', '.join(sorted(VALID_CATEGORIES))}."
|
||||
)
|
||||
@@ -306,30 +309,32 @@ class CheckMetadata(BaseModel):
|
||||
return check_id
|
||||
|
||||
@validator("CheckTitle", pre=True, always=True)
|
||||
def validate_check_title(cls, check_title):
|
||||
if len(check_title) > 150:
|
||||
raise ValueError(
|
||||
f"CheckTitle must not exceed 150 characters, got {len(check_title)} characters"
|
||||
)
|
||||
if check_title.startswith("Ensure"):
|
||||
raise ValueError(
|
||||
"CheckTitle must not start with 'Ensure'. Use a descriptive title that focuses on the security state."
|
||||
)
|
||||
def validate_check_title(cls, check_title, values):
|
||||
if values.get("Provider") not in EXTERNAL_TOOL_PROVIDERS:
|
||||
if len(check_title) > 150:
|
||||
raise ValueError(
|
||||
f"CheckTitle must not exceed 150 characters, got {len(check_title)} characters"
|
||||
)
|
||||
if check_title.startswith("Ensure"):
|
||||
raise ValueError(
|
||||
"CheckTitle must not start with 'Ensure'. Use a descriptive title that focuses on the security state."
|
||||
)
|
||||
return check_title
|
||||
|
||||
@validator("RelatedUrl", pre=True, always=True)
|
||||
def validate_related_url(cls, related_url):
|
||||
if related_url:
|
||||
def validate_related_url(cls, related_url, values):
|
||||
if related_url and values.get("Provider") not in EXTERNAL_TOOL_PROVIDERS:
|
||||
raise ValueError("RelatedUrl must be empty. This field is deprecated.")
|
||||
return related_url
|
||||
|
||||
@validator("Remediation")
|
||||
def validate_recommendation_url(remediation):
|
||||
url = remediation.Recommendation.Url
|
||||
if url and not url.startswith("https://hub.prowler.com/"):
|
||||
raise ValueError(
|
||||
f"Remediation Recommendation URL must point to Prowler Hub (https://hub.prowler.com/...), got '{url}'."
|
||||
)
|
||||
def validate_recommendation_url(cls, remediation, values):
|
||||
if values.get("Provider") not in EXTERNAL_TOOL_PROVIDERS:
|
||||
url = remediation.Recommendation.Url
|
||||
if url and not url.startswith("https://hub.prowler.com/"):
|
||||
raise ValueError(
|
||||
f"Remediation Recommendation URL must point to Prowler Hub (https://hub.prowler.com/...), got '{url}'."
|
||||
)
|
||||
return remediation
|
||||
|
||||
@validator("CheckType", pre=True, always=True)
|
||||
@@ -362,19 +367,21 @@ class CheckMetadata(BaseModel):
|
||||
return check_type
|
||||
|
||||
@validator("Description", pre=True, always=True)
|
||||
def validate_description(cls, description):
|
||||
if len(description) > 400:
|
||||
raise ValueError(
|
||||
f"Description must not exceed 400 characters, got {len(description)} characters"
|
||||
)
|
||||
def validate_description(cls, description, values):
|
||||
if values.get("Provider") not in EXTERNAL_TOOL_PROVIDERS:
|
||||
if len(description) > 400:
|
||||
raise ValueError(
|
||||
f"Description must not exceed 400 characters, got {len(description)} characters"
|
||||
)
|
||||
return description
|
||||
|
||||
@validator("Risk", pre=True, always=True)
|
||||
def validate_risk(cls, risk):
|
||||
if len(risk) > 400:
|
||||
raise ValueError(
|
||||
f"Risk must not exceed 400 characters, got {len(risk)} characters"
|
||||
)
|
||||
def validate_risk(cls, risk, values):
|
||||
if values.get("Provider") not in EXTERNAL_TOOL_PROVIDERS:
|
||||
if len(risk) > 400:
|
||||
raise ValueError(
|
||||
f"Risk must not exceed 400 characters, got {len(risk)} characters"
|
||||
)
|
||||
return risk
|
||||
|
||||
@validator("ResourceGroup", pre=True, always=True)
|
||||
|
||||
@@ -383,7 +383,7 @@ class ImageProvider(Provider):
|
||||
"Description", finding.get("Title", "")
|
||||
)
|
||||
finding_status = "FAIL"
|
||||
finding_categories = ["vulnerability"]
|
||||
finding_categories = ["vulnerabilities"]
|
||||
elif "RuleID" in finding:
|
||||
# Secret finding
|
||||
finding_id = finding["RuleID"]
|
||||
@@ -433,10 +433,13 @@ class ImageProvider(Provider):
|
||||
},
|
||||
"Recommendation": {
|
||||
"Text": remediation_text,
|
||||
"Url": finding.get("PrimaryURL", ""),
|
||||
"Url": "",
|
||||
},
|
||||
},
|
||||
"Categories": finding_categories,
|
||||
"AdditionalURLs": (
|
||||
[url] if (url := finding.get("PrimaryURL", "")) else []
|
||||
),
|
||||
"DependsOn": [],
|
||||
"RelatedTo": [],
|
||||
"Notes": "",
|
||||
|
||||
@@ -2418,3 +2418,249 @@ class TestCheck:
|
||||
msg = str(excinfo.value)
|
||||
assert "!= class name" in msg
|
||||
assert "!= file name" in msg
|
||||
|
||||
|
||||
class TestExternalToolProviderValidatorBypass:
|
||||
"""Validators skip strict rules for external tool providers (image, iac, llm)."""
|
||||
|
||||
EXTERNAL_METADATA_BASE = {
|
||||
"Provider": "image",
|
||||
"CheckID": "CVE-2024-1234",
|
||||
"CheckTitle": "OpenSSL Buffer Overflow",
|
||||
"CheckType": ["Container Image Security"],
|
||||
"ServiceName": "container-image",
|
||||
"SubServiceName": "",
|
||||
"ResourceIdTemplate": "",
|
||||
"Severity": "high",
|
||||
"ResourceType": "container-image",
|
||||
"ResourceGroup": "container",
|
||||
"Description": "A buffer overflow vulnerability.",
|
||||
"Risk": "Remote code execution.",
|
||||
"RelatedUrl": "",
|
||||
"Remediation": {
|
||||
"Code": {
|
||||
"CLI": "",
|
||||
"NativeIaC": "",
|
||||
"Other": "",
|
||||
"Terraform": "",
|
||||
},
|
||||
"Recommendation": {
|
||||
"Text": "Upgrade openssl",
|
||||
"Url": "https://avd.aquasec.com/nvd/cve-2024-1234",
|
||||
},
|
||||
},
|
||||
"Categories": ["vulnerability"],
|
||||
"DependsOn": [],
|
||||
"RelatedTo": [],
|
||||
"Notes": "",
|
||||
}
|
||||
|
||||
def test_external_provider_allows_non_hub_recommendation_url(self):
|
||||
metadata = CheckMetadata(**self.EXTERNAL_METADATA_BASE)
|
||||
assert (
|
||||
metadata.Remediation.Recommendation.Url
|
||||
== "https://avd.aquasec.com/nvd/cve-2024-1234"
|
||||
)
|
||||
|
||||
def test_native_provider_rejects_non_hub_recommendation_url(self):
|
||||
data = {
|
||||
**self.EXTERNAL_METADATA_BASE,
|
||||
"Provider": "azure",
|
||||
"CheckID": "test_check",
|
||||
"ServiceName": "test",
|
||||
"CheckType": [],
|
||||
"Remediation": {
|
||||
"Code": {
|
||||
"CLI": "",
|
||||
"NativeIaC": "",
|
||||
"Other": "",
|
||||
"Terraform": "",
|
||||
},
|
||||
"Recommendation": {
|
||||
"Text": "Fix it",
|
||||
"Url": "https://avd.aquasec.com/nvd/cve-2024-1234",
|
||||
},
|
||||
},
|
||||
}
|
||||
with pytest.raises(ValidationError) as exc_info:
|
||||
CheckMetadata(**data)
|
||||
assert "Prowler Hub" in str(exc_info.value)
|
||||
|
||||
def test_external_provider_allows_long_description(self):
|
||||
data = {**self.EXTERNAL_METADATA_BASE, "Description": "A" * 500}
|
||||
metadata = CheckMetadata(**data)
|
||||
assert len(metadata.Description) == 500
|
||||
|
||||
def test_native_provider_rejects_long_description(self):
|
||||
data = {
|
||||
**self.EXTERNAL_METADATA_BASE,
|
||||
"Provider": "azure",
|
||||
"CheckID": "test_check",
|
||||
"ServiceName": "test",
|
||||
"CheckType": [],
|
||||
"Categories": ["encryption"],
|
||||
"Description": "A" * 401,
|
||||
"Remediation": {
|
||||
"Code": {
|
||||
"CLI": "",
|
||||
"NativeIaC": "",
|
||||
"Other": "",
|
||||
"Terraform": "",
|
||||
},
|
||||
"Recommendation": {
|
||||
"Text": "",
|
||||
"Url": "",
|
||||
},
|
||||
},
|
||||
}
|
||||
with pytest.raises(ValidationError) as exc_info:
|
||||
CheckMetadata(**data)
|
||||
assert "Description must not exceed 400 characters" in str(exc_info.value)
|
||||
|
||||
def test_external_provider_allows_long_risk(self):
|
||||
data = {**self.EXTERNAL_METADATA_BASE, "Risk": "R" * 500}
|
||||
metadata = CheckMetadata(**data)
|
||||
assert len(metadata.Risk) == 500
|
||||
|
||||
def test_native_provider_rejects_long_risk(self):
|
||||
data = {
|
||||
**self.EXTERNAL_METADATA_BASE,
|
||||
"Provider": "azure",
|
||||
"CheckID": "test_check",
|
||||
"ServiceName": "test",
|
||||
"CheckType": [],
|
||||
"Categories": ["encryption"],
|
||||
"Risk": "R" * 401,
|
||||
"Remediation": {
|
||||
"Code": {
|
||||
"CLI": "",
|
||||
"NativeIaC": "",
|
||||
"Other": "",
|
||||
"Terraform": "",
|
||||
},
|
||||
"Recommendation": {
|
||||
"Text": "",
|
||||
"Url": "",
|
||||
},
|
||||
},
|
||||
}
|
||||
with pytest.raises(ValidationError) as exc_info:
|
||||
CheckMetadata(**data)
|
||||
assert "Risk must not exceed 400 characters" in str(exc_info.value)
|
||||
|
||||
def test_external_provider_allows_long_check_title(self):
|
||||
data = {**self.EXTERNAL_METADATA_BASE, "CheckTitle": "T" * 200}
|
||||
metadata = CheckMetadata(**data)
|
||||
assert len(metadata.CheckTitle) == 200
|
||||
|
||||
def test_native_provider_rejects_long_check_title(self):
|
||||
data = {
|
||||
**self.EXTERNAL_METADATA_BASE,
|
||||
"Provider": "azure",
|
||||
"CheckID": "test_check",
|
||||
"ServiceName": "test",
|
||||
"CheckType": [],
|
||||
"Categories": ["encryption"],
|
||||
"CheckTitle": "T" * 151,
|
||||
"Remediation": {
|
||||
"Code": {
|
||||
"CLI": "",
|
||||
"NativeIaC": "",
|
||||
"Other": "",
|
||||
"Terraform": "",
|
||||
},
|
||||
"Recommendation": {
|
||||
"Text": "",
|
||||
"Url": "",
|
||||
},
|
||||
},
|
||||
}
|
||||
with pytest.raises(ValidationError) as exc_info:
|
||||
CheckMetadata(**data)
|
||||
assert "CheckTitle must not exceed 150 characters" in str(exc_info.value)
|
||||
|
||||
def test_external_provider_allows_non_standard_category(self):
|
||||
data = {**self.EXTERNAL_METADATA_BASE, "Categories": ["vulnerability"]}
|
||||
metadata = CheckMetadata(**data)
|
||||
assert metadata.Categories == ["vulnerability"]
|
||||
|
||||
def test_native_provider_rejects_non_standard_category(self):
|
||||
data = {
|
||||
**self.EXTERNAL_METADATA_BASE,
|
||||
"Provider": "azure",
|
||||
"CheckID": "test_check",
|
||||
"ServiceName": "test",
|
||||
"CheckType": [],
|
||||
"Categories": ["vulnerability"],
|
||||
"Remediation": {
|
||||
"Code": {
|
||||
"CLI": "",
|
||||
"NativeIaC": "",
|
||||
"Other": "",
|
||||
"Terraform": "",
|
||||
},
|
||||
"Recommendation": {
|
||||
"Text": "",
|
||||
"Url": "",
|
||||
},
|
||||
},
|
||||
}
|
||||
with pytest.raises(ValidationError) as exc_info:
|
||||
CheckMetadata(**data)
|
||||
assert "Invalid category" in str(exc_info.value)
|
||||
|
||||
def test_external_provider_allows_ensure_prefix_in_title(self):
|
||||
data = {
|
||||
**self.EXTERNAL_METADATA_BASE,
|
||||
"CheckTitle": "Ensure containers run as non-root",
|
||||
}
|
||||
metadata = CheckMetadata(**data)
|
||||
assert metadata.CheckTitle == "Ensure containers run as non-root"
|
||||
|
||||
def test_external_provider_allows_non_empty_related_url(self):
|
||||
data = {
|
||||
**self.EXTERNAL_METADATA_BASE,
|
||||
"RelatedUrl": "https://avd.aquasec.com/nvd/cve-2024-1234",
|
||||
}
|
||||
metadata = CheckMetadata(**data)
|
||||
assert metadata.RelatedUrl == "https://avd.aquasec.com/nvd/cve-2024-1234"
|
||||
|
||||
def test_native_provider_rejects_non_empty_related_url(self):
|
||||
data = {
|
||||
**self.EXTERNAL_METADATA_BASE,
|
||||
"Provider": "azure",
|
||||
"CheckID": "test_check",
|
||||
"ServiceName": "test",
|
||||
"CheckType": [],
|
||||
"Categories": ["encryption"],
|
||||
"RelatedUrl": "https://example.com",
|
||||
"Remediation": {
|
||||
"Code": {
|
||||
"CLI": "",
|
||||
"NativeIaC": "",
|
||||
"Other": "",
|
||||
"Terraform": "",
|
||||
},
|
||||
"Recommendation": {
|
||||
"Text": "",
|
||||
"Url": "",
|
||||
},
|
||||
},
|
||||
}
|
||||
with pytest.raises(ValidationError) as exc_info:
|
||||
CheckMetadata(**data)
|
||||
assert "RelatedUrl must be empty" in str(exc_info.value)
|
||||
|
||||
def test_all_external_providers_bypass(self):
|
||||
for provider in ("image", "iac", "llm"):
|
||||
data = {
|
||||
**self.EXTERNAL_METADATA_BASE,
|
||||
"Provider": provider,
|
||||
"Description": "D" * 500,
|
||||
"Risk": "R" * 500,
|
||||
"CheckTitle": "T" * 200,
|
||||
"Categories": ["vulnerability"],
|
||||
"RelatedUrl": "https://example.com/vuln",
|
||||
}
|
||||
metadata = CheckMetadata(**data)
|
||||
assert metadata.Provider == provider
|
||||
|
||||
@@ -143,7 +143,7 @@ class TestImageProvider:
|
||||
assert report.image_sha == "c1aabb73d233"
|
||||
assert report.resource_details == "alpine:3.18 (alpine 3.18.0)"
|
||||
assert report.region == "container"
|
||||
assert report.check_metadata.Categories == ["vulnerability"]
|
||||
assert report.check_metadata.Categories == ["vulnerabilities"]
|
||||
assert report.check_metadata.RelatedUrl == ""
|
||||
|
||||
def test_process_finding_secret(self):
|
||||
|
||||
Reference in New Issue
Block a user