feat(iac): add support for remote repos (#8193)

Co-authored-by: MrCloudSec <hello@mistercloudsec.com>
This commit is contained in:
Andoni Alonso
2025-07-15 16:08:27 +02:00
committed by GitHub
parent bf0013dae3
commit 4b62fdcf53
15 changed files with 471 additions and 50 deletions
+6 -26
View File
@@ -491,11 +491,15 @@ The provided credentials must have the appropriate permissions to perform all th
## Infrastructure as Code (IaC)
Prowler's Infrastructure as Code (IaC) provider enables you to scan local infrastructure code for security and compliance issues using [Checkov](https://www.checkov.io/). This provider supports a wide range of IaC frameworks and requires no cloud authentication.
Prowler's Infrastructure as Code (IaC) provider enables you to scan local or remote infrastructure code for security and compliance issues using [Checkov](https://www.checkov.io/). This provider supports a wide range of IaC frameworks and requires no cloud authentication for local scans.
### Authentication
The IaC provider does not require any authentication or credentials since it scans local files directly. This makes it ideal for CI/CD pipelines and local development environments.
- For local scans, no authentication is required.
- For remote repository scans, authentication can be provided via:
- [**GitHub Username and Personal Access Token (PAT)**](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens#creating-a-personal-access-token-classic)
- [**GitHub OAuth App Token**](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens#creating-a-fine-grained-personal-access-token)
- [**Git URL**](https://git-scm.com/docs/git-clone#_git_urls)
### Supported Frameworks
@@ -515,27 +519,3 @@ The IaC provider leverages Checkov to support multiple frameworks, including:
- Kustomize
- OpenAPI
- SAST, SCA (Software Composition Analysis)
### Usage
To run Prowler with the IaC provider, use the `iac` flag. You can specify the directory to scan, frameworks to include, and paths to exclude.
#### Basic Example
```console
prowler iac --scan-path ./my-iac-directory
```
#### Specify Frameworks
Scan only Terraform and Kubernetes files:
```console
prowler iac --scan-path ./my-iac-directory --frameworks terraform kubernetes
```
#### Exclude Paths
```console
prowler iac --scan-path ./my-iac-directory --exclude-path ./my-iac-directory/test,./my-iac-directory/examples
```
+16 -3
View File
@@ -614,12 +614,23 @@ prowler github --github-app-id app_id --github-app-key app_key
#### Infrastructure as Code (IaC)
Prowler's Infrastructure as Code (IaC) provider enables you to scan local infrastructure code for security and compliance issues using [Checkov](https://www.checkov.io/). This provider supports a wide range of IaC frameworks, allowing you to assess your code before deployment.
Prowler's Infrastructure as Code (IaC) provider enables you to scan local or remote infrastructure code for security and compliance issues using [Checkov](https://www.checkov.io/). This provider supports a wide range of IaC frameworks, allowing you to assess your code before deployment.
```console
# Scan a directory for IaC files
prowler iac --scan-path ./my-iac-directory
# Scan a remote GitHub repository (public or private)
prowler iac --scan-repository-url https://github.com/user/repo.git
# Authenticate to a private repo with GitHub username and PAT
prowler iac --scan-repository-url https://github.com/user/repo.git \
--github-username <username> --personal-access-token <token>
# Authenticate to a private repo with OAuth App Token
prowler iac --scan-repository-url https://github.com/user/repo.git \
--oauth-app-token <oauth_token>
# Specify frameworks to scan (default: all)
prowler iac --scan-path ./my-iac-directory --frameworks terraform kubernetes
@@ -628,8 +639,10 @@ prowler iac --scan-path ./my-iac-directory --exclude-path ./my-iac-directory/tes
```
???+ note
- The IaC provider does not require cloud authentication
- It is ideal for CI/CD pipelines and local development environments
- `--scan-path` and `--scan-repository-url` are mutually exclusive; only one can be specified at a time.
- For remote repository scans, authentication can be provided via CLI flags or environment variables (`GITHUB_OAUTH_APP_TOKEN`, `GITHUB_USERNAME`, `GITHUB_PERSONAL_ACCESS_TOKEN`). CLI flags take precedence.
- The IaC provider does not require cloud authentication for local scans.
- It is ideal for CI/CD pipelines and local development environments.
- For more details on supported frameworks and rules, see the [Checkov documentation](https://www.checkov.io/1.Welcome/Quick%20Start.html)
See more details about IaC scanning in the [IaC Tutorial](tutorials/iac/getting-started-iac.md) section.
+37 -6
View File
@@ -1,6 +1,6 @@
# Getting Started with the IaC Provider
Prowler's Infrastructure as Code (IaC) provider enables you to scan local infrastructure code for security and compliance issues using [Checkov](https://www.checkov.io/). This provider supports a wide range of IaC frameworks, allowing you to assess your code before deployment.
Prowler's Infrastructure as Code (IaC) provider enables you to scan local or remote infrastructure code for security and compliance issues using [Checkov](https://www.checkov.io/). This provider supports a wide range of IaC frameworks, allowing you to assess your code before deployment.
## Supported Frameworks
@@ -23,21 +23,50 @@ The IaC provider leverages Checkov to support multiple frameworks, including:
## How It Works
- The IaC provider scans your local directory (or a specified path) for supported IaC files.
- No cloud credentials or authentication are required.
- The IaC provider scans your local directory (or a specified path) for supported IaC files, or scan a remote repository.
- No cloud credentials or authentication are required for local scans.
- For remote repository scans, authentication can be provided via [git URL](https://git-scm.com/docs/git-clone#_git_urls), CLI flags or environment variables.
- Mutelist logic is handled by Checkov, not Prowler.
- Results are output in the same formats as other Prowler providers (CSV, JSON, HTML, etc.).
## Usage
To run Prowler with the IaC provider, use the `iac` argument. You can specify the directory to scan, frameworks to include, and paths to exclude.
To run Prowler with the IaC provider, use the `iac` argument. You can specify the directory or repository to scan, frameworks to include, and paths to exclude.
### Basic Example
### Scan a Local Directory (default)
```sh
prowler iac --scan-path ./my-iac-directory
```
### Scan a Remote GitHub Repository
```sh
prowler iac --scan-repository-url https://github.com/user/repo.git
```
#### Authentication for Remote Private Repositories
You can provide authentication for private repositories using one of the following methods:
- **GitHub Username and Personal Access Token (PAT):**
```sh
prowler iac --scan-repository-url https://github.com/user/repo.git \
--github-username <username> --personal-access-token <token>
```
- **GitHub OAuth App Token:**
```sh
prowler iac --scan-repository-url https://github.com/user/repo.git \
--oauth-app-token <oauth_token>
```
- If not provided via CLI, the following environment variables will be used (in order of precedence):
- `GITHUB_OAUTH_APP_TOKEN`
- `GITHUB_USERNAME` and `GITHUB_PERSONAL_ACCESS_TOKEN`
- If neither CLI flags nor environment variables are set, the scan will attempt to clone without authentication or using the provided in the [git URL](https://git-scm.com/docs/git-clone#_git_urls).
#### Mutually Exclusive Flags
- `--scan-path` and `--scan-repository-url` are mutually exclusive. Only one can be specified at a time.
### Specify Frameworks
Scan only Terraform and Kubernetes files:
@@ -62,6 +91,8 @@ prowler iac --scan-path ./iac --output-formats csv json html
## Notes
- The IaC provider does not require cloud authentication.
- The IaC provider does not require cloud authentication for local scans.
- For remote repository scans, authentication is optional but required for private repos.
- CLI flags override environment variables for authentication.
- It is ideal for CI/CD pipelines and local development environments.
- For more details on supported frameworks and rules, see the [Checkov documentation](https://www.checkov.io/1.Welcome/Quick%20Start.html).
Generated
+49 -1
View File
@@ -1940,6 +1940,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.10"
@@ -6624,4 +6672,4 @@ type = ["pytest-mypy"]
[metadata]
lock-version = "2.1"
python-versions = ">3.9.1,<3.13"
content-hash = "4b0eee5566caf8e9d1e2e6fe8ac37733b29dd4275c2d65ac5291fa3acd514d9e"
content-hash = "7a3f5d9a2b06322b3c4b65d1010116f84ea5e725693e51316ffeb23d4ed09c96"
+1
View File
@@ -11,6 +11,7 @@ All notable changes to the **Prowler SDK** are documented in this file.
- `vm_linux_enforce_ssh_authentication` check for Azure provider [(#8149)](https://github.com/prowler-cloud/prowler/pull/8149)
- `vm_ensure_using_approved_images` check for Azure provider [(#8168)](https://github.com/prowler-cloud/prowler/pull/8168)
- `vm_scaleset_associated_load_balancer` check for Azure provider [(#8181)](https://github.com/prowler-cloud/prowler/pull/8181)
- Support for remote repository scanning in IaC provider [(#8193)](https://github.com/prowler-cloud/prowler/pull/8193)
- Add `test_connection` method to GitHub provider [(#8248)](https://github.com/prowler-cloud/prowler/pull/8248)
### Changed
+2 -4
View File
@@ -283,16 +283,14 @@ 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"] = provider.auth_method
output_data["account_uid"] = "iac"
output_data["account_name"] = "iac"
output_data["resource_name"] = check_output.resource_name
output_data["resource_uid"] = check_output.resource_name
output_data["region"] = check_output.resource_path
output_data["resource_line_range"] = check_output.resource_line_range
output_data["framework"] = (
check_output.check_metadata.ServiceName
) # TODO: can we get the framework from the check_output?
output_data["framework"] = check_output.check_metadata.ServiceName
# check_output Unique ID
# TODO: move this to a function
+2 -2
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> {provider.auth_method}
</li>
</ul>
</div>
+6 -2
View File
@@ -55,8 +55,12 @@ def display_summary_table(
entity_type = "Tenant Domain"
audited_entities = provider.identity.tenant_domain
elif provider.type == "iac":
entity_type = "Directory"
audited_entities = provider.scan_path
if provider.scan_repository_url:
entity_type = "Repository"
audited_entities = provider.scan_repository_url
else:
entity_type = "Directory"
audited_entities = provider.scan_path
# Check if there are findings and that they are not all MANUAL
if findings and not all(finding.status == "MANUAL" for finding in findings):
+4
View File
@@ -246,10 +246,14 @@ class Provider(ABC):
elif "iac" in provider_class_name.lower():
provider_class(
scan_path=arguments.scan_path,
scan_repository_url=arguments.scan_repository_url,
frameworks=arguments.frameworks,
exclude_path=arguments.exclude_path,
config_path=arguments.config_file,
fixer_config=fixer_config,
github_username=arguments.github_username,
personal_access_token=arguments.personal_access_token,
oauth_app_token=arguments.oauth_app_token,
)
except TypeError as error:
+130 -5
View File
@@ -1,7 +1,11 @@
import json
import shutil
import sys
import tempfile
from os import environ
from typing import List
from alive_progress import alive_bar
from checkov.ansible.runner import Runner as AnsibleRunner
from checkov.argo_workflows.runner import Runner as ArgoWorkflowsRunner
from checkov.arm.runner import Runner as ArmRunner
@@ -35,6 +39,7 @@ from checkov.terraform.runner import Runner as TerraformRunner
from checkov.terraform_json.runner import TerraformJsonRunner
from checkov.yaml_doc.runner import Runner as YamlDocRunner
from colorama import Fore, Style
from dulwich import porcelain
from prowler.config.config import (
default_config_file_path,
@@ -54,21 +59,56 @@ class IacProvider(Provider):
def __init__(
self,
scan_path: str = ".",
scan_repository_url: str = None,
frameworks: list[str] = ["all"],
exclude_path: list[str] = [],
config_path: str = None,
config_content: dict = None,
fixer_config: dict = {},
github_username: str = None,
personal_access_token: str = None,
oauth_app_token: str = None,
):
logger.info("Instantiating IAC Provider...")
self.scan_path = scan_path
self.scan_repository_url = scan_repository_url
self.frameworks = frameworks
self.exclude_path = exclude_path
self.region = "global"
self.audited_account = "local-iac"
self._session = None
self._identity = "prowler"
self._auth_method = "No auth"
if scan_repository_url:
oauth_app_token = oauth_app_token or environ.get("GITHUB_OAUTH_APP_TOKEN")
github_username = github_username or environ.get("GITHUB_USERNAME")
personal_access_token = personal_access_token or environ.get(
"GITHUB_PERSONAL_ACCESS_TOKEN"
)
if oauth_app_token:
self.oauth_app_token = oauth_app_token
self.github_username = None
self.personal_access_token = None
self._auth_method = "OAuth App Token"
logger.info("Using OAuth App Token for GitHub authentication")
elif github_username and personal_access_token:
self.github_username = github_username
self.personal_access_token = personal_access_token
self.oauth_app_token = None
self._auth_method = "Personal Access Token"
logger.info(
"Using GitHub username and personal access token for authentication"
)
else:
self.github_username = None
self.personal_access_token = None
self.oauth_app_token = None
logger.debug(
"No GitHub authentication method provided; proceeding without authentication."
)
# Audit Config
if config_content:
@@ -97,6 +137,10 @@ class IacProvider(Provider):
Provider.set_global_provider(self)
@property
def auth_method(self):
return self._auth_method
@property
def type(self):
return self._type
@@ -183,8 +227,72 @@ class IacProvider(Provider):
)
sys.exit(1)
def _clone_repository(
self,
repository_url: str,
github_username: str = None,
personal_access_token: str = None,
oauth_app_token: str = None,
) -> str:
"""
Clone a git repository to a temporary directory, supporting GitHub authentication.
"""
try:
if github_username and personal_access_token:
repository_url = repository_url.replace(
"https://github.com/",
f"https://{github_username}:{personal_access_token}@github.com/",
)
elif oauth_app_token:
repository_url = repository_url.replace(
"https://github.com/",
f"https://oauth2:{oauth_app_token}@github.com/",
)
temporary_directory = tempfile.mkdtemp()
logger.info(
f"Cloning repository {repository_url} into {temporary_directory}..."
)
with alive_bar(
ctrl_c=False,
bar="blocks",
spinner="classic",
stats=False,
enrich_print=False,
) as bar:
try:
bar.title = f"-> Cloning {repository_url}..."
porcelain.clone(repository_url, temporary_directory, depth=1)
bar.title = "-> Repository cloned successfully!"
except Exception as clone_error:
bar.title = "-> Cloning failed!"
raise clone_error
return temporary_directory
except Exception as error:
logger.critical(
f"{error.__class__.__name__}:{error.__traceback__.tb_lineno} -- {error}"
)
def run(self) -> List[CheckReportIAC]:
return self.run_scan(self.scan_path, self.frameworks, self.exclude_path)
temp_dir = None
if self.scan_repository_url:
scan_dir = temp_dir = self._clone_repository(
self.scan_repository_url,
getattr(self, "github_username", None),
getattr(self, "personal_access_token", None),
getattr(self, "oauth_app_token", None),
)
else:
scan_dir = self.scan_path
try:
reports = self.run_scan(scan_dir, self.frameworks, self.exclude_path)
finally:
if temp_dir:
logger.info(f"Removing temporary directory {temp_dir}...")
shutil.rmtree(temp_dir)
return reports
def run_scan(
self, directory: str, frameworks: list[str], exclude_path: list[str]
@@ -249,15 +357,32 @@ class IacProvider(Provider):
sys.exit(1)
def print_credentials(self):
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}"
)
report_lines = [
f"Repository: {Fore.YELLOW}{self.scan_repository_url}{Style.RESET_ALL}",
]
else:
report_title = (
f"{Style.BRIGHT}Scanning local IaC directory:{Style.RESET_ALL}"
)
report_lines = [
f"Directory: {Fore.YELLOW}{self.scan_path}{Style.RESET_ALL}",
]
if self.exclude_path:
report_lines.append(
f"Excluded paths: {Fore.YELLOW}{', '.join(self.exclude_path)}{Style.RESET_ALL}"
)
report_lines.append(
f"Frameworks: {Fore.YELLOW}{', '.join(self.frameworks)}{Style.RESET_ALL}"
)
report_title = f"{Style.BRIGHT}Scanning local IaC directory:{Style.RESET_ALL}"
report_lines.append(
f"Authentication method: {Fore.YELLOW}{self.auth_method}{Style.RESET_ALL}"
)
print_boxes(report_lines, report_title)
@@ -44,8 +44,17 @@ def init_parser(self):
"-P",
dest="scan_path",
default=".",
help="Path to the folder containing your infrastructure-as-code files. Default: current directory",
help="Path to the folder containing your infrastructure-as-code files. Default: current directory. Mutually exclusive with --scan-repository-url.",
)
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. Mutually exclusive with --scan-path.",
)
iac_scan_subparser.add_argument(
"--frameworks",
"-f",
@@ -63,3 +72,38 @@ def init_parser(self):
default=[],
help="Comma-separated list of paths to exclude from the scan. Default: none",
)
iac_scan_subparser.add_argument(
"--github-username",
dest="github_username",
nargs="?",
default=None,
help="GitHub username for authenticated repository cloning (used with --personal-access-token). If not provided, will use GITHUB_USERNAME env var.",
)
iac_scan_subparser.add_argument(
"--personal-access-token",
dest="personal_access_token",
nargs="?",
default=None,
help="GitHub personal access token for authenticated repository cloning (used with --github-username). If not provided, will use GITHUB_PERSONAL_ACCESS_TOKEN env var.",
)
iac_scan_subparser.add_argument(
"--oauth-app-token",
dest="oauth_app_token",
nargs="?",
default=None,
help="GitHub OAuth app token for authenticated repository cloning. If not provided, will use GITHUB_OAUTH_APP_TOKEN env var.",
)
def validate_arguments(arguments):
scan_path = getattr(arguments, "scan_path", None)
scan_repository_url = getattr(arguments, "scan_repository_url", None)
if scan_path and scan_repository_url:
# If scan_path is set to default ("."), allow scan_repository_url
if scan_path != ".":
return (
False,
"--scan-path (-P) and --scan-repository-url (-R) are mutually exclusive. Please specify only one.",
)
return (True, "")
+1
View File
@@ -43,6 +43,7 @@ dependencies = [
"dash==3.1.1",
"dash-bootstrap-components==2.0.3",
"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",
+51
View File
@@ -506,6 +506,57 @@ 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"
provider.auth_method = "No auth"
# Mock check result
check_output = MagicMock()
check_output.file_path = "/path/to/iac/file.tf"
check_output.resource_name = "aws_s3_bucket.example"
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 == "No auth"
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()
+88
View File
@@ -1,3 +1,6 @@
import os
import tempfile
from unittest import mock
from unittest.mock import Mock, patch
import pytest
@@ -131,6 +134,63 @@ class TestIacProvider:
assert report.status == "FAIL"
assert report.check_metadata.RelatedUrl == ""
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, ["all"], [])
@mock.patch.dict(os.environ, {}, clear=True)
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, None, None, None)
mock_run_scan.assert_called_with(temp_dir, ["all"], [])
@mock.patch.dict(os.environ, {}, clear=True)
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
)
@mock.patch.dict(os.environ, {}, clear=True)
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
)
def test_iac_provider_process_check_medium_severity(self):
"""Test processing a medium severity check"""
provider = IacProvider()
@@ -543,3 +603,31 @@ class TestIacProvider:
mock_run_scan.assert_called_once_with(
"/custom/path", ["terraform"], ["exclude"]
)
@mock.patch("prowler.providers.iac.iac_provider.porcelain.clone")
@mock.patch("tempfile.mkdtemp", return_value="/tmp/fake-dir")
def test_clone_repository_no_auth(self, _mock_mkdtemp, mock_clone):
provider = IacProvider()
url = "https://github.com/user/repo.git"
provider._clone_repository(url)
mock_clone.assert_called_with(url, "/tmp/fake-dir", depth=1)
@mock.patch("prowler.providers.iac.iac_provider.porcelain.clone")
@mock.patch("tempfile.mkdtemp", return_value="/tmp/fake-dir")
def test_clone_repository_with_pat(self, _mock_mkdtemp, mock_clone):
provider = IacProvider()
url = "https://github.com/user/repo.git"
provider._clone_repository(
url, github_username="user", personal_access_token="token123"
)
expected_url = "https://user:token123@github.com/user/repo.git"
mock_clone.assert_called_with(expected_url, "/tmp/fake-dir", depth=1)
@mock.patch("prowler.providers.iac.iac_provider.porcelain.clone")
@mock.patch("tempfile.mkdtemp", return_value="/tmp/fake-dir")
def test_clone_repository_with_oauth(self, _mock_mkdtemp, mock_clone):
provider = IacProvider()
url = "https://github.com/user/repo.git"
provider._clone_repository(url, oauth_app_token="oauth456")
expected_url = "https://oauth2:oauth456@github.com/user/repo.git"
mock_clone.assert_called_with(expected_url, "/tmp/fake-dir", depth=1)
+33
View File
@@ -0,0 +1,33 @@
import types
def test_validate_arguments_mutual_exclusion():
from prowler.providers.iac.lib.arguments import arguments as iac_arguments
Args = types.SimpleNamespace
# Only scan_path (default)
args = Args(scan_path=".", scan_repository_url=None)
valid, msg = iac_arguments.validate_arguments(args)
assert valid
assert msg == ""
# Only scan_repository_url
args = Args(scan_path=".", scan_repository_url="https://github.com/test/repo")
valid, msg = iac_arguments.validate_arguments(args)
assert valid
assert msg == ""
# Both set, scan_path is not default
args = Args(
scan_path="/some/path", scan_repository_url="https://github.com/test/repo"
)
valid, msg = iac_arguments.validate_arguments(args)
assert not valid
assert "mutually exclusive" in msg
# Both set, scan_path is default (should allow)
args = Args(scan_path=".", scan_repository_url="https://github.com/test/repo")
valid, msg = iac_arguments.validate_arguments(args)
assert valid
assert msg == ""