Compare commits

...

4 Commits

Author SHA1 Message Date
Andoni A.
ae87997057 refactor 2025-06-23 19:44:18 +02:00
Andoni A.
8b88938eaa chore(iac): update tests 2025-06-23 14:47:00 +02:00
Andoni A.
0eeb534aed feat(iac): update html report 2025-06-23 12:24:08 +02:00
Andoni A.
8496d6e23c feat(iac): add git clone scan, without handling auth yet 2025-06-23 12:11:05 +02:00
9 changed files with 221 additions and 10 deletions

52
poetry.lock generated
View File

@@ -1,4 +1,4 @@
# This file is automatically @generated by Poetry 2.1.2 and should not be changed by hand.
# This file is automatically @generated by Poetry 2.1.3 and should not be changed by hand.
[[package]]
name = "about-time"
@@ -1919,6 +1919,54 @@ files = [
{file = "dpath-2.1.3.tar.gz", hash = "sha256:d1a7a0e6427d0a4156c792c82caf1f0109603f68ace792e36ca4596fd2cb8d9d"},
]
[[package]]
name = "dulwich"
version = "0.23.0"
description = "Python Git Library"
optional = false
python-versions = ">=3.9"
groups = ["main"]
files = [
{file = "dulwich-0.23.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c13b0d5a9009cde23ecb8cb201df6e23e2a7a82c5e2d6ba6443fbb322c9befc6"},
{file = "dulwich-0.23.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:a68faf8612bf93de1285048d6ad13160f0fb3c5596a86e694e78f4e212886fa5"},
{file = "dulwich-0.23.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:d971566826f16ec67c70641c1fbdb337323aa5b533799bc5a4641f4750e73b36"},
{file = "dulwich-0.23.0-cp310-cp310-win32.whl", hash = "sha256:27d970adf539806dfc4fe3e4c9e8dc6ebf0318977a56e24d22f13413535a51ba"},
{file = "dulwich-0.23.0-cp310-cp310-win_amd64.whl", hash = "sha256:025178533e884ffdb0d9d8db4b8870745d438cbfecb782fd1b56c3b6438e86cf"},
{file = "dulwich-0.23.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d68498fdda13ab00791b483daab3bcfe9f9721c037aa458695e6ad81640c57cc"},
{file = "dulwich-0.23.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:cb7bb930b12471a1cfcea4b3d25a671dc0ad32573f0ad25684684298959a1527"},
{file = "dulwich-0.23.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:a2abbce32fd2bc7902bcc5f69b10bf22576810de21651baaa864b78fd7aec261"},
{file = "dulwich-0.23.0-cp311-cp311-win32.whl", hash = "sha256:9e3151f10ce2a9ff91bca64c74345217f53bdd947dc958032343822009832f7a"},
{file = "dulwich-0.23.0-cp311-cp311-win_amd64.whl", hash = "sha256:3ae9f1d9dc92d4e9a3f89ba2c55221f7b6442c5dd93b3f6f539a3c9eb3f37bdd"},
{file = "dulwich-0.23.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:52cdef66a7994d29528ca79ca59452518bbba3fd56a9c61c61f6c467c1c7956e"},
{file = "dulwich-0.23.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:d473888a6ab9ed5d4a4c3f053cbe5b77f72d54b6efdf5688fed76094316e571e"},
{file = "dulwich-0.23.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:19fcf20224c641a61c774da92f098fbaae9938c7e17a52841e64092adf7e78f9"},
{file = "dulwich-0.23.0-cp312-cp312-win32.whl", hash = "sha256:7fc8b76b704ef35cd001e993e3aa4e1d666a2064bf467c07c560f12b2959dcaf"},
{file = "dulwich-0.23.0-cp312-cp312-win_amd64.whl", hash = "sha256:cb0566b888b578325350b4d67c61a0de35d417e9877560e3a6df88cae4576a59"},
{file = "dulwich-0.23.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:624e2223c8b705b3a217f9c8d3bfed3a573093be0b0ba033c46cba8411fb9630"},
{file = "dulwich-0.23.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:b4eaf326d15bb3fc5316c777b0312f0fe02f6f82a4368cd971d0ce2167b7ec34"},
{file = "dulwich-0.23.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:d754afaf7c133a015c75cc2be11703138b4be932e0eeeb2c70add56083f31109"},
{file = "dulwich-0.23.0-cp313-cp313-win32.whl", hash = "sha256:ac53ec438bde3c1f479782c34240479b36cd47230d091979137b7ecc12c0242e"},
{file = "dulwich-0.23.0-cp313-cp313-win_amd64.whl", hash = "sha256:50d3b4ba45671fb8b7d2afbd02c10b4edbc3290a1f92260e64098b409e9ca35c"},
{file = "dulwich-0.23.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:d8e18ea3fa49f10932077f39c0b960b5045870c550c3d7c74f3cfaac09457cd6"},
{file = "dulwich-0.23.0-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:3e6df0eb8cca21f210e3ddce2ccb64482646893dbec2fee9f3411d037595bf7b"},
{file = "dulwich-0.23.0-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:90c0064d7df8e7fe83d3a03c7d60b9e07a92698b18442f926199b2c3f0bf34d4"},
{file = "dulwich-0.23.0-cp39-cp39-win32.whl", hash = "sha256:84eef513aba501cbc1f223863f3b4b351fe732d3fb590cab9bdf5d33eb1a1248"},
{file = "dulwich-0.23.0-cp39-cp39-win_amd64.whl", hash = "sha256:dce943da48217c26e15790fd6df62d27a7f1d067102780351ebf2635fc0ba482"},
{file = "dulwich-0.23.0-py3-none-any.whl", hash = "sha256:d8da6694ca332bb48775e35ee2215aa4673821164a91b83062f699c69f7cd135"},
{file = "dulwich-0.23.0.tar.gz", hash = "sha256:0aa6c2489dd5e978b27e9b75983b7331a66c999f0efc54ebe37cab808ed322ae"},
]
[package.dependencies]
urllib3 = ">=1.25"
[package.extras]
dev = ["dissolve (>=0.1.1)", "mypy (==1.16.0)", "ruff (==0.11.13)"]
fastimport = ["fastimport"]
https = ["urllib3 (>=1.24.1)"]
merge = ["merge3"]
paramiko = ["paramiko"]
pgp = ["gpg"]
[[package]]
name = "durationpy"
version = "0.9"
@@ -6603,4 +6651,4 @@ type = ["pytest-mypy"]
[metadata]
lock-version = "2.1"
python-versions = ">3.9.1,<3.13"
content-hash = "d72c55b52949ba94f0c68004d5b778edb69514a05bbb7aba8d641b5058a99fd5"
content-hash = "87c36db93b05afc33296b8a60a6eeb1955edf2555fdcd1c1431c6b66fe6d6a8e"

View File

@@ -283,7 +283,7 @@ class Finding(BaseModel):
output_data["region"] = check_output.location
elif provider.type == "iac":
output_data["auth_method"] = "local" # Until we support remote repos
output_data["auth_method"] = "None" # Until we support remote repos
output_data["account_uid"] = "iac"
output_data["account_name"] = "iac"
output_data["resource_name"] = check_output.resource["resource"]

View File

@@ -710,7 +710,7 @@ class HTML(Output):
<ul class="list-group
list-group-flush">
<li class="list-group-item">
<b>IAC path:</b> {provider.scan_path}
{"<b>IAC repository URL:</b> " + provider.scan_repository_url if provider.scan_repository_url else "<b>IAC path:</b> " + provider.scan_path}
</li>
</ul>
</div>
@@ -723,7 +723,7 @@ class HTML(Output):
<ul class="list-group
list-group-flush">
<li class="list-group-item">
<b>IAC authentication method:</b> local
<b>IAC authentication method:</b> None
</li>
</ul>
</div>

View File

@@ -246,6 +246,7 @@ class Provider(ABC):
elif "iac" in provider_class_name.lower():
provider_class(
scan_path=arguments.scan_path,
scan_repository_url=arguments.scan_repository_url,
config_path=arguments.config_file,
fixer_config=fixer_config,
)

View File

@@ -1,9 +1,12 @@
import json
import shutil
import subprocess
import sys
import tempfile
from typing import List
from colorama import Fore, Style
from dulwich import porcelain
from prowler.config.config import (
default_config_file_path,
@@ -23,6 +26,7 @@ class IacProvider(Provider):
def __init__(
self,
scan_path: str = ".",
scan_repository_url: str = None,
config_path: str = None,
config_content: dict = None,
fixer_config: dict = {},
@@ -30,6 +34,7 @@ class IacProvider(Provider):
logger.info("Instantiating IAC Provider...")
self.scan_path = scan_path
self.scan_repository_url = scan_repository_url
self.region = "global"
self.audited_account = "local-iac"
self._session = None
@@ -146,8 +151,38 @@ class IacProvider(Provider):
report.muted = True
return report
def _clone_repository(self, repository_url: str) -> str:
"""
Clone a git repository to a temporary directory.
"""
try:
temporary_directory = tempfile.mkdtemp()
logger.info(
f"Cloning repository {repository_url} into {temporary_directory}..."
)
porcelain.clone(repository_url, temporary_directory)
return temporary_directory
except Exception as error:
logger.critical(
f"{error.__class__.__name__}:{error.__traceback__.tb_lineno} -- {error}"
)
sys.exit(1)
def run(self) -> List[CheckReportIAC]:
return self.run_scan(self.scan_path)
temp_dir = None
if self.scan_repository_url:
scan_dir = temp_dir = self._clone_repository(self.scan_repository_url)
else:
scan_dir = self.scan_path
try:
reports = self.run_scan(scan_dir)
finally:
if temp_dir:
logger.info(f"Removing temporary directory {temp_dir}...")
shutil.rmtree(temp_dir)
return reports
def run_scan(self, directory: str) -> List[CheckReportIAC]:
try:
@@ -211,8 +246,20 @@ class IacProvider(Provider):
sys.exit(1)
def print_credentials(self):
report_lines = [
f"Directory: {Fore.YELLOW}{self.scan_path}{Style.RESET_ALL}",
]
report_title = f"{Style.BRIGHT}Scanning local IaC directory:{Style.RESET_ALL}"
if self.scan_repository_url:
report_lines = [
f"Repository: {Fore.YELLOW}{self.scan_repository_url}{Style.RESET_ALL}",
]
else:
report_lines = [
f"Directory: {Fore.YELLOW}{self.scan_path}{Style.RESET_ALL}",
]
if self.scan_repository_url:
report_title = (
f"{Style.BRIGHT}Scanning remote IaC repository:{Style.RESET_ALL}"
)
else:
report_title = (
f"{Style.BRIGHT}Scanning local IaC directory:{Style.RESET_ALL}"
)
print_boxes(report_lines, report_title)

View File

@@ -13,3 +13,11 @@ def init_parser(self):
default=".",
help="Path to the folder containing your infrastructure-as-code files. Default: current directory",
)
iac_scan_subparser.add_argument(
"--scan-repository-url",
"-R",
dest="scan_repository_url",
default=None,
help="URL to the repository containing your infrastructure-as-code files.",
)

View File

@@ -41,6 +41,7 @@ dependencies = [
"dash==2.18.2",
"dash-bootstrap-components==1.6.0",
"detect-secrets==1.5.0",
"dulwich==0.23.0",
"google-api-python-client==2.163.0",
"google-auth-httplib2>=0.1,<0.3",
"jsonschema==4.23.0",

View File

@@ -506,6 +506,55 @@ class TestFinding:
assert finding_output.metadata.Notes == "mock_notes"
assert finding_output.metadata.Compliance == []
def test_generate_output_iac_remote(self):
# Mock provider
provider = MagicMock()
provider.type = "iac"
provider.scan_repository_url = "https://github.com/user/repo"
# Mock check result
check_output = MagicMock()
check_output.file_path = "/path/to/iac/file.tf"
check_output.resource_path = "/path/to/iac/file.tf"
check_output.file_line_range = [1, 5]
check_output.resource = {
"resource": "aws_s3_bucket.example",
"value": {},
}
check_output.resource_details = "test_resource_details"
check_output.status = Status.PASS
check_output.status_extended = "mock_status_extended"
check_output.muted = False
check_output.check_metadata = mock_check_metadata(provider="iac")
check_output.compliance = {}
# Mock output options
output_options = MagicMock()
output_options.unix_timestamp = False
# Generate the finding
finding_output = Finding.generate_output(provider, check_output, output_options)
# Finding
assert isinstance(finding_output, Finding)
assert finding_output.auth_method == "None"
assert finding_output.resource_name == "aws_s3_bucket.example"
assert finding_output.resource_uid == "aws_s3_bucket.example"
assert finding_output.region == "/path/to/iac/file.tf"
assert finding_output.status == Status.PASS
assert finding_output.status_extended == "mock_status_extended"
assert finding_output.muted is False
# Metadata
assert finding_output.metadata.Provider == "iac"
assert finding_output.metadata.CheckID == "mock_check_id"
assert finding_output.metadata.CheckTitle == "mock_check_title"
assert finding_output.metadata.CheckType == []
assert finding_output.metadata.CheckAliases == []
assert finding_output.metadata.ServiceName == "mock_service_name"
assert finding_output.metadata.SubServiceName == ""
assert finding_output.metadata.ResourceIdTemplate == ""
def assert_keys_lowercase(self, d):
for k, v in d.items():
assert k.islower()

View File

@@ -1,3 +1,5 @@
import tempfile
from unittest import mock
from unittest.mock import MagicMock, patch
import pytest
@@ -130,3 +132,58 @@ class TestIacProvider:
reports = provider.run_scan("/test/directory")
assert len(reports) == 0
def test_provider_run_local_scan(self):
scan_path = "."
provider = IacProvider(scan_path=scan_path)
with mock.patch(
"prowler.providers.iac.iac_provider.IacProvider.run_scan",
) as mock_run_scan:
provider.run()
mock_run_scan.assert_called_with(scan_path)
def test_provider_run_remote_scan(self):
scan_repository_url = "https://github.com/user/repo"
provider = IacProvider(scan_repository_url=scan_repository_url)
with tempfile.TemporaryDirectory() as temp_dir:
with (
mock.patch(
"prowler.providers.iac.iac_provider.IacProvider._clone_repository",
return_value=temp_dir,
) as mock_clone,
mock.patch(
"prowler.providers.iac.iac_provider.IacProvider.run_scan"
) as mock_run_scan,
):
provider.run()
mock_clone.assert_called_with(scan_repository_url)
mock_run_scan.assert_called_with(temp_dir)
def test_print_credentials_local(self):
scan_path = "/path/to/scan"
provider = IacProvider(scan_path=scan_path)
with mock.patch("builtins.print") as mock_print:
provider.print_credentials()
assert any(
f"Directory: \x1b[33m{scan_path}\x1b[0m" in call.args[0]
for call in mock_print.call_args_list
)
assert any(
"Scanning local IaC directory:" in call.args[0]
for call in mock_print.call_args_list
)
def test_print_credentials_remote(self):
repo_url = "https://github.com/user/repo"
provider = IacProvider(scan_repository_url=repo_url)
with mock.patch("builtins.print") as mock_print:
provider.print_credentials()
assert any(
f"Repository: \x1b[33m{repo_url}\x1b[0m" in call.args[0]
for call in mock_print.call_args_list
)
assert any(
"Scanning remote IaC repository:" in call.args[0]
for call in mock_print.call_args_list
)