mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-04-03 14:06:23 +00:00
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:
@@ -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,
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -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}"
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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):
|
||||
|
||||
Reference in New Issue
Block a user