mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-07-04 19:21:51 +00:00
feat(iac): add support for remote repos (#8193)
Co-authored-by: MrCloudSec <hello@mistercloudsec.com>
This commit is contained in:
@@ -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
@@ -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.
|
||||
|
||||
@@ -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
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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, "")
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 == ""
|
||||
Reference in New Issue
Block a user