diff --git a/prowler/providers/common/provider.py b/prowler/providers/common/provider.py index df141fd312..f638fe47f2 100644 --- a/prowler/providers/common/provider.py +++ b/prowler/providers/common/provider.py @@ -279,6 +279,7 @@ class Provider(ABC): images=arguments.images, image_list_file=arguments.image_list_file, scanners=arguments.scanners, + image_config_scanners=arguments.image_config_scanners, trivy_severity=arguments.trivy_severity, ignore_unfixed=arguments.ignore_unfixed, timeout=arguments.timeout, diff --git a/prowler/providers/image/exceptions/exceptions.py b/prowler/providers/image/exceptions/exceptions.py index f040210ad6..591358d829 100644 --- a/prowler/providers/image/exceptions/exceptions.py +++ b/prowler/providers/image/exceptions/exceptions.py @@ -46,6 +46,10 @@ class ImageBaseException(ProwlerException): "message": "Invalid container image name.", "remediation": "Use a valid image reference (e.g., 'alpine:3.18', 'registry.example.com/repo/image:tag').", }, + (9010, "ImageInvalidConfigScannerError"): { + "message": "Invalid image config scanner type.", + "remediation": "Use valid image config scanners: misconfig, secret.", + }, } def __init__(self, code, file=None, original_exception=None, message=None): @@ -149,3 +153,12 @@ class ImageInvalidNameError(ImageBaseException): super().__init__( 9009, file=file, original_exception=original_exception, message=message ) + + +class ImageInvalidConfigScannerError(ImageBaseException): + """Exception raised when an invalid image config scanner type is provided.""" + + def __init__(self, file=None, original_exception=None, message=None): + super().__init__( + 9010, file=file, original_exception=original_exception, message=message + ) diff --git a/prowler/providers/image/image_provider.py b/prowler/providers/image/image_provider.py index d66c9f280c..1e11651253 100644 --- a/prowler/providers/image/image_provider.py +++ b/prowler/providers/image/image_provider.py @@ -20,6 +20,7 @@ from prowler.providers.common.models import Audit_Metadata, Connection from prowler.providers.common.provider import Provider from prowler.providers.image.exceptions.exceptions import ( ImageFindingProcessingError, + ImageInvalidConfigScannerError, ImageInvalidNameError, ImageInvalidScannerError, ImageInvalidSeverityError, @@ -31,6 +32,7 @@ from prowler.providers.image.exceptions.exceptions import ( ImageTrivyBinaryNotFoundError, ) from prowler.providers.image.lib.arguments.arguments import ( + IMAGE_CONFIG_SCANNERS_CHOICES, SCANNERS_CHOICES, SEVERITY_CHOICES, ) @@ -57,6 +59,7 @@ class ImageProvider(Provider): images: list[str] | None = None, image_list_file: str | None = None, scanners: list[str] | None = None, + image_config_scanners: list[str] | None = None, trivy_severity: list[str] | None = None, ignore_unfixed: bool = False, timeout: str = "5m", @@ -69,6 +72,9 @@ class ImageProvider(Provider): self.images = images if images is not None else [] self.image_list_file = image_list_file self.scanners = scanners if scanners is not None else ["vuln", "secret"] + self.image_config_scanners = ( + image_config_scanners if image_config_scanners is not None else [] + ) self.trivy_severity = trivy_severity if trivy_severity is not None else [] self.ignore_unfixed = ignore_unfixed self.timeout = timeout @@ -171,6 +177,13 @@ class ImageProvider(Provider): message=f"Invalid scanner: '{scanner}'. Valid options: {', '.join(SCANNERS_CHOICES)}.", ) + for config_scanner in self.image_config_scanners: + if config_scanner not in IMAGE_CONFIG_SCANNERS_CHOICES: + raise ImageInvalidConfigScannerError( + file=__file__, + message=f"Invalid image config scanner: '{config_scanner}'. Valid options: {', '.join(IMAGE_CONFIG_SCANNERS_CHOICES)}.", + ) + for severity in self.trivy_severity: if severity not in SEVERITY_CHOICES: raise ImageInvalidSeverityError( @@ -403,6 +416,11 @@ class ImageProvider(Provider): self.timeout, ] + if self.image_config_scanners: + trivy_command.extend( + ["--image-config-scanners", ",".join(self.image_config_scanners)] + ) + if self.trivy_severity: trivy_command.extend(["--severity", ",".join(self.trivy_severity)]) @@ -595,6 +613,11 @@ class ImageProvider(Provider): f"Scanners: {Fore.YELLOW}{', '.join(self.scanners)}{Style.RESET_ALL}" ) + if self.image_config_scanners: + report_lines.append( + f"Image config scanners: {Fore.YELLOW}{', '.join(self.image_config_scanners)}{Style.RESET_ALL}" + ) + if self.trivy_severity: report_lines.append( f"Severity filter: {Fore.YELLOW}{', '.join(self.trivy_severity)}{Style.RESET_ALL}" diff --git a/prowler/providers/image/lib/arguments/arguments.py b/prowler/providers/image/lib/arguments/arguments.py index e27efac97e..3809aec667 100644 --- a/prowler/providers/image/lib/arguments/arguments.py +++ b/prowler/providers/image/lib/arguments/arguments.py @@ -5,6 +5,11 @@ SCANNERS_CHOICES = [ "license", ] +IMAGE_CONFIG_SCANNERS_CHOICES = [ + "misconfig", + "secret", +] + SEVERITY_CHOICES = [ "CRITICAL", "HIGH", @@ -50,6 +55,15 @@ def init_parser(self): help="Trivy scanners to use. Default: vuln, secret. Available: vuln, secret, misconfig, license", ) + scan_config_group.add_argument( + "--image-config-scanners", + dest="image_config_scanners", + nargs="+", + default=[], + choices=IMAGE_CONFIG_SCANNERS_CHOICES, + help="Trivy image config scanners (scans Dockerfile-level metadata). Available: misconfig, secret", + ) + scan_config_group.add_argument( "--trivy-severity", dest="trivy_severity", diff --git a/tests/providers/image/image_provider_test.py b/tests/providers/image/image_provider_test.py index 2cc8fe7a2d..75bebaefc5 100644 --- a/tests/providers/image/image_provider_test.py +++ b/tests/providers/image/image_provider_test.py @@ -6,6 +6,7 @@ import pytest from prowler.lib.check.models import CheckReportImage from prowler.providers.image.exceptions.exceptions import ( + ImageInvalidConfigScannerError, ImageInvalidNameError, ImageInvalidScannerError, ImageInvalidSeverityError, @@ -48,6 +49,7 @@ class TestImageProvider: assert provider.type == "image" assert provider.images == ["alpine:3.18"] assert provider.scanners == ["vuln", "secret"] + assert provider.image_config_scanners == [] assert provider.trivy_severity == [] assert provider.ignore_unfixed is False assert provider.timeout == "5m" @@ -456,6 +458,53 @@ class TestImageProviderInputValidation: "UNKNOWN", ] + def test_image_config_scanners_defaults_to_empty(self): + """Test that image_config_scanners defaults to an empty list.""" + provider = _make_provider() + assert provider.image_config_scanners == [] + + def test_valid_image_config_scanners(self): + """Test that valid image config scanners are accepted.""" + provider = _make_provider(image_config_scanners=["misconfig", "secret"]) + assert provider.image_config_scanners == ["misconfig", "secret"] + + def test_invalid_image_config_scanner_raises_error(self): + """Test that an invalid image config scanner raises ImageInvalidConfigScannerError.""" + with pytest.raises(ImageInvalidConfigScannerError): + _make_provider(image_config_scanners=["misconfig", "vuln"]) + + @patch("subprocess.run") + def test_trivy_command_includes_image_config_scanners(self, mock_subprocess): + """Test that Trivy command includes --image-config-scanners when set.""" + provider = _make_provider(image_config_scanners=["misconfig", "secret"]) + mock_subprocess.return_value = MagicMock( + returncode=0, stdout=get_empty_trivy_output(), stderr="" + ) + + for _ in provider._scan_single_image("alpine:3.18"): + pass + + call_args = mock_subprocess.call_args[0][0] + assert "--image-config-scanners" in call_args + idx = call_args.index("--image-config-scanners") + assert call_args[idx + 1] == "misconfig,secret" + + @patch("subprocess.run") + def test_trivy_command_omits_image_config_scanners_when_empty( + self, mock_subprocess + ): + """Test that Trivy command omits --image-config-scanners when empty.""" + provider = _make_provider(image_config_scanners=[]) + mock_subprocess.return_value = MagicMock( + returncode=0, stdout=get_empty_trivy_output(), stderr="" + ) + + for _ in provider._scan_single_image("alpine:3.18"): + pass + + call_args = mock_subprocess.call_args[0][0] + assert "--image-config-scanners" not in call_args + class TestImageProviderErrorCategorization: def test_categorize_auth_failure(self):