From bfd82180328ebbc74e2d505e32de92a18c3c56c2 Mon Sep 17 00:00:00 2001 From: "Andoni A." <14891798+andoniaf@users.noreply.github.com> Date: Thu, 12 Feb 2026 09:20:26 +0100 Subject: [PATCH] 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 --- prowler/__main__.py | 13 +++--- prowler/config/config.py | 4 ++ prowler/lib/check/models.py | 22 ++++----- prowler/lib/outputs/finding.py | 2 +- prowler/lib/outputs/summary_table.py | 5 +- .../providers/image/exceptions/__init__.py | 25 ---------- .../providers/image/exceptions/exceptions.py | 46 +++++++++---------- 7 files changed, 45 insertions(+), 72 deletions(-) diff --git a/prowler/__main__.py b/prowler/__main__.py index 6ceea64e03..d99c688e9e 100644 --- a/prowler/__main__.py +++ b/prowler/__main__.py @@ -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): diff --git a/prowler/config/config.py b/prowler/config/config.py index 7219e9a7eb..77674c7f93 100644 --- a/prowler/config/config.py +++ b/prowler/config/config.py @@ -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__))) diff --git a/prowler/lib/check/models.py b/prowler/lib/check/models.py index 499f75c457..648b962039 100644 --- a/prowler/lib/check/models.py +++ b/prowler/lib/check/models.py @@ -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", "") diff --git a/prowler/lib/outputs/finding.py b/prowler/lib/outputs/finding.py index 39c3b0269a..82f64850f8 100644 --- a/prowler/lib/outputs/finding.py +++ b/prowler/lib/outputs/finding.py @@ -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( diff --git a/prowler/lib/outputs/summary_table.py b/prowler/lib/outputs/summary_table.py index 2e1b499977..6a1d978d7c 100644 --- a/prowler/lib/outputs/summary_table.py +++ b/prowler/lib/outputs/summary_table.py @@ -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): diff --git a/prowler/providers/image/exceptions/__init__.py b/prowler/providers/image/exceptions/__init__.py index 6daaa639b5..e69de29bb2 100644 --- a/prowler/providers/image/exceptions/__init__.py +++ b/prowler/providers/image/exceptions/__init__.py @@ -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", -] diff --git a/prowler/providers/image/exceptions/exceptions.py b/prowler/providers/image/exceptions/exceptions.py index 591358d829..0e115a83ec 100644 --- a/prowler/providers/image/exceptions/exceptions.py +++ b/prowler/providers/image/exceptions/exceptions.py @@ -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 )