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)
|
- 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)
|
- 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)
|
Compliance: Optional[list[Any]] = Field(default_factory=list)
|
||||||
|
|
||||||
@validator("Categories", each_item=True, pre=True, always=True)
|
@validator("Categories", each_item=True, pre=True, always=True)
|
||||||
def valid_category(value):
|
def valid_category(cls, value, values):
|
||||||
if not isinstance(value, str):
|
if not isinstance(value, str):
|
||||||
raise ValueError("Categories must be a list of strings")
|
raise ValueError("Categories must be a list of strings")
|
||||||
value_lower = value.lower()
|
value_lower = value.lower()
|
||||||
@@ -253,7 +253,10 @@ class CheckMetadata(BaseModel):
|
|||||||
raise ValueError(
|
raise ValueError(
|
||||||
f"Invalid category: {value}. Categories can only contain lowercase letters, numbers and hyphen '-'"
|
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(
|
raise ValueError(
|
||||||
f"Invalid category: '{value_lower}'. Must be one of: {', '.join(sorted(VALID_CATEGORIES))}."
|
f"Invalid category: '{value_lower}'. Must be one of: {', '.join(sorted(VALID_CATEGORIES))}."
|
||||||
)
|
)
|
||||||
@@ -306,30 +309,32 @@ class CheckMetadata(BaseModel):
|
|||||||
return check_id
|
return check_id
|
||||||
|
|
||||||
@validator("CheckTitle", pre=True, always=True)
|
@validator("CheckTitle", pre=True, always=True)
|
||||||
def validate_check_title(cls, check_title):
|
def validate_check_title(cls, check_title, values):
|
||||||
if len(check_title) > 150:
|
if values.get("Provider") not in EXTERNAL_TOOL_PROVIDERS:
|
||||||
raise ValueError(
|
if len(check_title) > 150:
|
||||||
f"CheckTitle must not exceed 150 characters, got {len(check_title)} characters"
|
raise ValueError(
|
||||||
)
|
f"CheckTitle must not exceed 150 characters, got {len(check_title)} characters"
|
||||||
if check_title.startswith("Ensure"):
|
)
|
||||||
raise ValueError(
|
if check_title.startswith("Ensure"):
|
||||||
"CheckTitle must not start with 'Ensure'. Use a descriptive title that focuses on the security state."
|
raise ValueError(
|
||||||
)
|
"CheckTitle must not start with 'Ensure'. Use a descriptive title that focuses on the security state."
|
||||||
|
)
|
||||||
return check_title
|
return check_title
|
||||||
|
|
||||||
@validator("RelatedUrl", pre=True, always=True)
|
@validator("RelatedUrl", pre=True, always=True)
|
||||||
def validate_related_url(cls, related_url):
|
def validate_related_url(cls, related_url, values):
|
||||||
if related_url:
|
if related_url and values.get("Provider") not in EXTERNAL_TOOL_PROVIDERS:
|
||||||
raise ValueError("RelatedUrl must be empty. This field is deprecated.")
|
raise ValueError("RelatedUrl must be empty. This field is deprecated.")
|
||||||
return related_url
|
return related_url
|
||||||
|
|
||||||
@validator("Remediation")
|
@validator("Remediation")
|
||||||
def validate_recommendation_url(remediation):
|
def validate_recommendation_url(cls, remediation, values):
|
||||||
url = remediation.Recommendation.Url
|
if values.get("Provider") not in EXTERNAL_TOOL_PROVIDERS:
|
||||||
if url and not url.startswith("https://hub.prowler.com/"):
|
url = remediation.Recommendation.Url
|
||||||
raise ValueError(
|
if url and not url.startswith("https://hub.prowler.com/"):
|
||||||
f"Remediation Recommendation URL must point to Prowler Hub (https://hub.prowler.com/...), got '{url}'."
|
raise ValueError(
|
||||||
)
|
f"Remediation Recommendation URL must point to Prowler Hub (https://hub.prowler.com/...), got '{url}'."
|
||||||
|
)
|
||||||
return remediation
|
return remediation
|
||||||
|
|
||||||
@validator("CheckType", pre=True, always=True)
|
@validator("CheckType", pre=True, always=True)
|
||||||
@@ -362,19 +367,21 @@ class CheckMetadata(BaseModel):
|
|||||||
return check_type
|
return check_type
|
||||||
|
|
||||||
@validator("Description", pre=True, always=True)
|
@validator("Description", pre=True, always=True)
|
||||||
def validate_description(cls, description):
|
def validate_description(cls, description, values):
|
||||||
if len(description) > 400:
|
if values.get("Provider") not in EXTERNAL_TOOL_PROVIDERS:
|
||||||
raise ValueError(
|
if len(description) > 400:
|
||||||
f"Description must not exceed 400 characters, got {len(description)} characters"
|
raise ValueError(
|
||||||
)
|
f"Description must not exceed 400 characters, got {len(description)} characters"
|
||||||
|
)
|
||||||
return description
|
return description
|
||||||
|
|
||||||
@validator("Risk", pre=True, always=True)
|
@validator("Risk", pre=True, always=True)
|
||||||
def validate_risk(cls, risk):
|
def validate_risk(cls, risk, values):
|
||||||
if len(risk) > 400:
|
if values.get("Provider") not in EXTERNAL_TOOL_PROVIDERS:
|
||||||
raise ValueError(
|
if len(risk) > 400:
|
||||||
f"Risk must not exceed 400 characters, got {len(risk)} characters"
|
raise ValueError(
|
||||||
)
|
f"Risk must not exceed 400 characters, got {len(risk)} characters"
|
||||||
|
)
|
||||||
return risk
|
return risk
|
||||||
|
|
||||||
@validator("ResourceGroup", pre=True, always=True)
|
@validator("ResourceGroup", pre=True, always=True)
|
||||||
|
|||||||
@@ -383,7 +383,7 @@ class ImageProvider(Provider):
|
|||||||
"Description", finding.get("Title", "")
|
"Description", finding.get("Title", "")
|
||||||
)
|
)
|
||||||
finding_status = "FAIL"
|
finding_status = "FAIL"
|
||||||
finding_categories = ["vulnerability"]
|
finding_categories = ["vulnerabilities"]
|
||||||
elif "RuleID" in finding:
|
elif "RuleID" in finding:
|
||||||
# Secret finding
|
# Secret finding
|
||||||
finding_id = finding["RuleID"]
|
finding_id = finding["RuleID"]
|
||||||
@@ -433,10 +433,13 @@ class ImageProvider(Provider):
|
|||||||
},
|
},
|
||||||
"Recommendation": {
|
"Recommendation": {
|
||||||
"Text": remediation_text,
|
"Text": remediation_text,
|
||||||
"Url": finding.get("PrimaryURL", ""),
|
"Url": "",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"Categories": finding_categories,
|
"Categories": finding_categories,
|
||||||
|
"AdditionalURLs": (
|
||||||
|
[url] if (url := finding.get("PrimaryURL", "")) else []
|
||||||
|
),
|
||||||
"DependsOn": [],
|
"DependsOn": [],
|
||||||
"RelatedTo": [],
|
"RelatedTo": [],
|
||||||
"Notes": "",
|
"Notes": "",
|
||||||
|
|||||||
@@ -2418,3 +2418,249 @@ class TestCheck:
|
|||||||
msg = str(excinfo.value)
|
msg = str(excinfo.value)
|
||||||
assert "!= class name" in msg
|
assert "!= class name" in msg
|
||||||
assert "!= file 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.image_sha == "c1aabb73d233"
|
||||||
assert report.resource_details == "alpine:3.18 (alpine 3.18.0)"
|
assert report.resource_details == "alpine:3.18 (alpine 3.18.0)"
|
||||||
assert report.region == "container"
|
assert report.region == "container"
|
||||||
assert report.check_metadata.Categories == ["vulnerability"]
|
assert report.check_metadata.Categories == ["vulnerabilities"]
|
||||||
assert report.check_metadata.RelatedUrl == ""
|
assert report.check_metadata.RelatedUrl == ""
|
||||||
|
|
||||||
def test_process_finding_secret(self):
|
def test_process_finding_secret(self):
|
||||||
|
|||||||
Reference in New Issue
Block a user