fix(image): address PR review feedback

- Change exception error codes from 9000 to 11000 range to avoid Cloudflare conflict
- Add EXTERNAL_TOOL_PROVIDERS constant to replace hardcoded provider tuples
- Add resource_id field to CheckReportImage populated from Trivy finding IDs
- Use resource_id for resource_uid in finding output
- Join image names in summary table instead of showing count
- Clear exceptions __init__.py re-exports
This commit is contained in:
Andoni A. committed 2026-02-12 09:20:26 +01:00
1 parent 43ad2f705c
commit bfd8218032
7 files changed
+45 -72

No files matched your search

+7 -6
View File
@@ -8,6 +8,7 @@ from colorama import Fore, Style
from colorama import init as colorama_init
from prowler.config.config import (
EXTERNAL_TOOL_PROVIDERS,
csv_file_suffix,
get_available_compliance_frameworks,
html_file_suffix,
@@ -208,8 +209,8 @@ def prowler():
# Load compliance frameworks
logger.debug("Loading compliance frameworks from .json files")
# Skip compliance frameworks for IAC, LLM, and Image providers
if provider not in ("iac", "llm", "image"):
# Skip compliance frameworks for external-tool providers
if provider not in EXTERNAL_TOOL_PROVIDERS:
bulk_compliance_frameworks = Compliance.get_bulk(provider)
# Complete checks metadata with the compliance framework specification
bulk_checks_metadata = update_checks_metadata_with_compliance(
@@ -266,8 +267,8 @@ def prowler():
if not args.only_logs:
global_provider.print_credentials()
# Skip service and check loading for IAC, LLM, and Image providers
if provider not in ("iac", "llm", "image"):
# Skip service and check loading for external-tool providers
if provider not in EXTERNAL_TOOL_PROVIDERS:
# Import custom checks from folder
if checks_folder:
custom_checks = parse_checks_from_folder(global_provider, checks_folder)
@@ -379,8 +380,8 @@ def prowler():
# Execute checks
findings = []
if provider in ("iac", "llm", "image"):
# For IAC, LLM, and Image providers, run the scan directly
if provider in EXTERNAL_TOOL_PROVIDERS:
# For external-tool providers, run the scan directly
if provider == "llm":
def streaming_callback(findings_batch):
+4
View File
@@ -66,6 +66,10 @@ class Provider(str, Enum):
IMAGE = "image"
# Providers that delegate scanning to an external tool (e.g. Trivy, promptfoo)
# and bypass standard check/service loading.
EXTERNAL_TOOL_PROVIDERS = frozenset({"iac", "llm", "image"})
# Compliance
actual_directory = pathlib.Path(os.path.dirname(os.path.realpath(__file__)))
+9 -13
View File
@@ -10,7 +10,7 @@ from typing import Any, Dict, Optional, Set
from pydantic.v1 import BaseModel, Field, ValidationError, validator
from pydantic.v1.error_wrappers import ErrorWrapper
from prowler.config.config import Provider
from prowler.config.config import EXTERNAL_TOOL_PROVIDERS, Provider
from prowler.lib.check.compliance_models import Compliance
from prowler.lib.check.utils import recover_checks_from_provider
from prowler.lib.logger import logger
@@ -159,12 +159,7 @@ class CheckMetadata(BaseModel):
raise ValueError("ServiceName must be a non-empty string")
check_id = values.get("CheckID")
if (
check_id
and values.get("Provider") != "iac"
and values.get("Provider") != "llm"
and values.get("Provider") != "image"
):
if check_id and values.get("Provider") not in EXTERNAL_TOOL_PROVIDERS:
service_from_check_id = check_id.split("_")[0]
if service_name != service_from_check_id:
raise ValueError(
@@ -180,12 +175,7 @@ class CheckMetadata(BaseModel):
if not check_id:
raise ValueError("CheckID must be a non-empty string")
if (
check_id
and values.get("Provider") != "iac"
and values.get("Provider") != "llm"
and values.get("Provider") != "image"
):
if check_id and values.get("Provider") not in EXTERNAL_TOOL_PROVIDERS:
if "-" in check_id:
raise ValueError(
f"CheckID {check_id} contains a hyphen, which is not allowed"
@@ -872,6 +862,7 @@ class CheckReportImage(Check_Report):
"""Contains the Container Image Check's finding information using Trivy."""
resource_name: str
resource_id: str
image_digest: str
package_name: str
installed_version: str
@@ -898,6 +889,11 @@ class CheckReportImage(Check_Report):
super().__init__(metadata, finding)
self.resource_name = image_name
self.resource_id = (
finding.get("VulnerabilityID", "")
or finding.get("RuleID", "")
or finding.get("ID", "")
)
self.image_digest = finding.get("PkgID", "")
self.package_name = finding.get("PkgName", "")
self.installed_version = finding.get("InstalledVersion", "")
+1 -1
View File
@@ -387,7 +387,7 @@ class Finding(BaseModel):
output_data["resource_name"] = getattr(
check_output, "resource_name", ""
)
output_data["resource_uid"] = getattr(check_output, "resource_name", "")
output_data["resource_uid"] = getattr(check_output, "resource_id", "")
output_data["region"] = getattr(check_output, "region", "container")
output_data["package_name"] = getattr(check_output, "package_name", "")
output_data["installed_version"] = getattr(
+1 -4
View File
@@ -95,10 +95,7 @@ def display_summary_table(
)
elif provider.type == "image":
entity_type = "Image"
if len(provider.images) == 1:
audited_entities = provider.images[0]
else:
audited_entities = f"{len(provider.images)} images"
audited_entities = ", ".join(provider.images)
# Check if there are findings and that they are not all MANUAL
if findings and not all(finding.status == "MANUAL" for finding in findings):
@@ -1,25 +0,0 @@
from prowler.providers.image.exceptions.exceptions import (
ImageBaseException,
ImageFindingProcessingError,
ImageInvalidScannerError,
ImageInvalidSeverityError,
ImageInvalidTimeoutError,
ImageListFileNotFoundError,
ImageListFileReadError,
ImageNoImagesProvidedError,
ImageScanError,
ImageTrivyBinaryNotFoundError,
)
__all__ = [
"ImageBaseException",
"ImageFindingProcessingError",
"ImageInvalidScannerError",
"ImageInvalidSeverityError",
"ImageInvalidTimeoutError",
"ImageListFileNotFoundError",
"ImageListFileReadError",
"ImageNoImagesProvidedError",
"ImageScanError",
"ImageTrivyBinaryNotFoundError",
]
@@ -1,52 +1,52 @@
from prowler.exceptions.exceptions import ProwlerException
# Exceptions codes from 9000 to 9999 are reserved for Image exceptions
# Exceptions codes from 11000 to 11999 are reserved for Image exceptions
class ImageBaseException(ProwlerException):
"""Base class for Image provider errors."""
IMAGE_ERROR_CODES = {
(9000, "ImageNoImagesProvidedError"): {
(11000, "ImageNoImagesProvidedError"): {
"message": "No container images provided for scanning.",
"remediation": "Provide at least one image using --image or --image-list-file.",
},
(9001, "ImageListFileNotFoundError"): {
(11001, "ImageListFileNotFoundError"): {
"message": "Image list file not found.",
"remediation": "Ensure the image list file exists at the specified path.",
},
(9002, "ImageListFileReadError"): {
(11002, "ImageListFileReadError"): {
"message": "Error reading image list file.",
"remediation": "Check file permissions and format. The file should contain one image per line.",
},
(9003, "ImageFindingProcessingError"): {
(11003, "ImageFindingProcessingError"): {
"message": "Error processing image scan finding.",
"remediation": "Check the Trivy output format and ensure the finding structure is valid.",
},
(9004, "ImageTrivyBinaryNotFoundError"): {
(11004, "ImageTrivyBinaryNotFoundError"): {
"message": "Trivy binary not found.",
"remediation": "Install Trivy from https://trivy.dev/latest/getting-started/installation/",
},
(9005, "ImageScanError"): {
(11005, "ImageScanError"): {
"message": "Error scanning container image.",
"remediation": "Check the image name and ensure it is accessible.",
},
(9006, "ImageInvalidTimeoutError"): {
(11006, "ImageInvalidTimeoutError"): {
"message": "Invalid timeout format.",
"remediation": "Use a valid timeout like '5m', '300s', or '1h'.",
},
(9007, "ImageInvalidScannerError"): {
(11007, "ImageInvalidScannerError"): {
"message": "Invalid scanner type.",
"remediation": "Use valid scanners: vuln, secret, misconfig, license.",
},
(9008, "ImageInvalidSeverityError"): {
(11008, "ImageInvalidSeverityError"): {
"message": "Invalid severity level.",
"remediation": "Use valid severities: CRITICAL, HIGH, MEDIUM, LOW, UNKNOWN.",
},
(9009, "ImageInvalidNameError"): {
(11009, "ImageInvalidNameError"): {
"message": "Invalid container image name.",
"remediation": "Use a valid image reference (e.g., 'alpine:3.18', 'registry.example.com/repo/image:tag').",
},
(9010, "ImageInvalidConfigScannerError"): {
(11010, "ImageInvalidConfigScannerError"): {
"message": "Invalid image config scanner type.",
"remediation": "Use valid image config scanners: misconfig, secret.",
},
@@ -70,7 +70,7 @@ class ImageNoImagesProvidedError(ImageBaseException):
def __init__(self, file=None, original_exception=None, message=None):
super().__init__(
9000, file=file, original_exception=original_exception, message=message
11000, file=file, original_exception=original_exception, message=message
)
@@ -79,7 +79,7 @@ class ImageListFileNotFoundError(ImageBaseException):
def __init__(self, file=None, original_exception=None, message=None):
super().__init__(
9001, file=file, original_exception=original_exception, message=message
11001, file=file, original_exception=original_exception, message=message
)
@@ -88,7 +88,7 @@ class ImageListFileReadError(ImageBaseException):
def __init__(self, file=None, original_exception=None, message=None):
super().__init__(
9002, file=file, original_exception=original_exception, message=message
11002, file=file, original_exception=original_exception, message=message
)
@@ -97,7 +97,7 @@ class ImageFindingProcessingError(ImageBaseException):
def __init__(self, file=None, original_exception=None, message=None):
super().__init__(
9003, file=file, original_exception=original_exception, message=message
11003, file=file, original_exception=original_exception, message=message
)
@@ -106,7 +106,7 @@ class ImageTrivyBinaryNotFoundError(ImageBaseException):
def __init__(self, file=None, original_exception=None, message=None):
super().__init__(
9004, file=file, original_exception=original_exception, message=message
11004, file=file, original_exception=original_exception, message=message
)
@@ -115,7 +115,7 @@ class ImageScanError(ImageBaseException):
def __init__(self, file=None, original_exception=None, message=None):
super().__init__(
9005, file=file, original_exception=original_exception, message=message
11005, file=file, original_exception=original_exception, message=message
)
@@ -124,7 +124,7 @@ class ImageInvalidTimeoutError(ImageBaseException):
def __init__(self, file=None, original_exception=None, message=None):
super().__init__(
9006, file=file, original_exception=original_exception, message=message
11006, file=file, original_exception=original_exception, message=message
)
@@ -133,7 +133,7 @@ class ImageInvalidScannerError(ImageBaseException):
def __init__(self, file=None, original_exception=None, message=None):
super().__init__(
9007, file=file, original_exception=original_exception, message=message
11007, file=file, original_exception=original_exception, message=message
)
@@ -142,7 +142,7 @@ class ImageInvalidSeverityError(ImageBaseException):
def __init__(self, file=None, original_exception=None, message=None):
super().__init__(
9008, file=file, original_exception=original_exception, message=message
11008, file=file, original_exception=original_exception, message=message
)
@@ -151,7 +151,7 @@ class ImageInvalidNameError(ImageBaseException):
def __init__(self, file=None, original_exception=None, message=None):
super().__init__(
9009, file=file, original_exception=original_exception, message=message
11009, file=file, original_exception=original_exception, message=message
)
@@ -160,5 +160,5 @@ class ImageInvalidConfigScannerError(ImageBaseException):
def __init__(self, file=None, original_exception=None, message=None):
super().__init__(
9010, file=file, original_exception=original_exception, message=message
11010, file=file, original_exception=original_exception, message=message
)