Merge branch 'feat/PROWLER-939-stage-1-image-provider-mvp-for-cli' of https://github.com/prowler-cloud/prowler into feat/PROWLER-941-stage-2-b-image-provider-cli-docs

This commit is contained in:
Andoni A.
2026-02-09 15:56:10 +01:00
5 changed files with 100 additions and 0 deletions

View File

@@ -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,

View File

@@ -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
)

View File

@@ -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}"

View File

@@ -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",

View File

@@ -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):