Compare commits

...

11 Commits

Author SHA1 Message Date
Andoni A.
a5fe98b0d2 refactor: scan IaC skip validations 2025-09-24 09:12:19 +02:00
Andoni A.
d47ff104d6 remove: commit local dev changes, revert after sending PR 2025-09-12 10:58:07 +02:00
Andoni A.
d09adb3edd chore: update comments 2025-09-12 10:56:31 +02:00
Andoni A.
f76847d653 chore: update trivy 2025-09-12 10:38:01 +02:00
Andoni A.
06cf9ed0cc wip: set defaults 2025-09-12 10:38:01 +02:00
Andoni A.
75390c0979 wip: add IaC to UI 2025-09-12 10:37:49 +02:00
Andoni A.
27f5c9591b wip: fix IaC scan mapping 2025-09-12 10:36:42 +02:00
Andoni A.
dbff60576b wip: add IaC to API 2025-09-12 10:36:42 +02:00
Andoni A.
ff22d198d0 wip 2025-09-12 10:36:41 +02:00
Andoni A.
5c2b867546 wip: do not use progress bar in Celery workers 2025-09-12 10:36:41 +02:00
Andoni A.
ae2200131f wip 2025-09-12 10:36:41 +02:00
34 changed files with 740 additions and 64 deletions

2
.env
View File

@@ -74,7 +74,7 @@ DJANGO_SETTINGS_MODULE=config.django.production
DJANGO_LOGGING_FORMATTER=human_readable
# Select one of [DEBUG|INFO|WARNING|ERROR|CRITICAL]
# Applies to both Django and Celery Workers
DJANGO_LOGGING_LEVEL=INFO
DJANGO_LOGGING_LEVEL=DEBUG
# Defaults to the maximum available based on CPU cores if not set.
DJANGO_WORKERS=4
# Token lifetime is in minutes

View File

@@ -2,6 +2,13 @@
All notable changes to the **Prowler API** are documented in this file.
## [Unreleased]
### Added
- IaC (Infrastructure as Code) provider support for remote repositories [(#TBD)](https://github.com/prowler-cloud/prowler/pull/TBD)
---
## [1.13.0] (Prowler 5.12.0)
### Added

View File

@@ -36,6 +36,25 @@ RUN ARCH=$(uname -m) && \
ln -s /opt/microsoft/powershell/7/pwsh /usr/bin/pwsh && \
rm /tmp/powershell.tar.gz
# Install Trivy for IaC scanning
ARG TRIVY_VERSION=0.66.0
RUN ARCH=$(uname -m) && \
if [ "$ARCH" = "x86_64" ]; then \
TRIVY_ARCH="Linux-64bit" ; \
elif [ "$ARCH" = "aarch64" ]; then \
TRIVY_ARCH="Linux-ARM64" ; \
else \
echo "Unsupported architecture for Trivy: $ARCH" && exit 1 ; \
fi && \
wget --progress=dot:giga "https://github.com/aquasecurity/trivy/releases/download/v${TRIVY_VERSION}/trivy_${TRIVY_VERSION}_${TRIVY_ARCH}.tar.gz" -O /tmp/trivy.tar.gz && \
tar zxf /tmp/trivy.tar.gz -C /tmp && \
mv /tmp/trivy /usr/local/bin/trivy && \
chmod +x /usr/local/bin/trivy && \
rm /tmp/trivy.tar.gz && \
# Create trivy cache directory with proper permissions
mkdir -p /tmp/.cache/trivy && \
chmod 777 /tmp/.cache/trivy
# Add prowler user
RUN addgroup --gid 1000 prowler && \
adduser --uid 1000 --gid 1000 --disabled-password --gecos "" prowler

View File

@@ -24,7 +24,7 @@ dependencies = [
"drf-spectacular-jsonapi==0.5.1",
"gunicorn==23.0.0",
"lxml==5.3.2",
"prowler @ git+https://github.com/prowler-cloud/prowler.git@master",
"prowler @ git+https://github.com/prowler-cloud/prowler.git@iac-in-the-app",
"psycopg2-binary==2.9.9",
"pytest-celery[redis] (>=1.0.1,<2.0.0)",
"sentry-sdk[django] (>=2.20.0,<3.0.0)",

View File

@@ -0,0 +1,34 @@
# Generated by Django 5.1.10 on 2025-09-09 09:25
from django.db import migrations
import api.db_utils
class Migration(migrations.Migration):
dependencies = [
("api", "0047_remove_integration_unique_configuration_per_tenant"),
]
operations = [
migrations.AlterField(
model_name="provider",
name="provider",
field=api.db_utils.ProviderEnumField(
choices=[
("aws", "AWS"),
("azure", "Azure"),
("gcp", "GCP"),
("kubernetes", "Kubernetes"),
("m365", "M365"),
("github", "GitHub"),
("iac", "IaC"),
],
default="aws",
),
),
migrations.RunSQL(
"ALTER TYPE provider ADD VALUE IF NOT EXISTS 'iac';",
reverse_sql=migrations.RunSQL.noop,
),
]

View File

@@ -215,6 +215,7 @@ class Provider(RowLevelSecurityProtectedModel):
KUBERNETES = "kubernetes", _("Kubernetes")
M365 = "m365", _("M365")
GITHUB = "github", _("GitHub")
IAC = "iac", _("IaC")
@staticmethod
def validate_aws_uid(value):
@@ -285,6 +286,19 @@ class Provider(RowLevelSecurityProtectedModel):
pointer="/data/attributes/uid",
)
@staticmethod
def validate_iac_uid(value):
# Validate that it's a valid repository URL (git URL format)
if not re.match(
r"^(https?://|git@|ssh://)[^\s/]+[^\s]*\.git$|^(https?://)[^\s/]+[^\s]*$",
value,
):
raise ModelValidationError(
detail="IaC provider ID must be a valid repository URL (e.g., https://github.com/user/repo or https://github.com/user/repo.git).",
code="iac-uid",
pointer="/data/attributes/uid",
)
id = models.UUIDField(primary_key=True, default=uuid4, editable=False)
inserted_at = models.DateTimeField(auto_now_add=True, editable=False)
updated_at = models.DateTimeField(auto_now=True, editable=False)

View File

@@ -601,6 +601,7 @@ paths:
- azure
- gcp
- github
- iac
- kubernetes
- m365
description: |-
@@ -610,6 +611,7 @@ paths:
* `kubernetes` - Kubernetes
* `m365` - M365
* `github` - GitHub
* `iac` - IaC
- in: query
name: filter[provider_type__in]
schema:
@@ -633,6 +635,7 @@ paths:
* `kubernetes` - Kubernetes
* `m365` - M365
* `github` - GitHub
* `iac` - IaC
explode: false
style: form
- in: query
@@ -1128,6 +1131,7 @@ paths:
- azure
- gcp
- github
- iac
- kubernetes
- m365
description: |-
@@ -1137,6 +1141,7 @@ paths:
* `kubernetes` - Kubernetes
* `m365` - M365
* `github` - GitHub
* `iac` - IaC
- in: query
name: filter[provider_type__in]
schema:
@@ -1160,6 +1165,7 @@ paths:
* `kubernetes` - Kubernetes
* `m365` - M365
* `github` - GitHub
* `iac` - IaC
explode: false
style: form
- in: query
@@ -1563,6 +1569,7 @@ paths:
- azure
- gcp
- github
- iac
- kubernetes
- m365
description: |-
@@ -1572,6 +1579,7 @@ paths:
* `kubernetes` - Kubernetes
* `m365` - M365
* `github` - GitHub
* `iac` - IaC
- in: query
name: filter[provider_type__in]
schema:
@@ -1595,6 +1603,7 @@ paths:
* `kubernetes` - Kubernetes
* `m365` - M365
* `github` - GitHub
* `iac` - IaC
explode: false
style: form
- in: query
@@ -1996,6 +2005,7 @@ paths:
- azure
- gcp
- github
- iac
- kubernetes
- m365
description: |-
@@ -2005,6 +2015,7 @@ paths:
* `kubernetes` - Kubernetes
* `m365` - M365
* `github` - GitHub
* `iac` - IaC
- in: query
name: filter[provider_type__in]
schema:
@@ -2028,6 +2039,7 @@ paths:
* `kubernetes` - Kubernetes
* `m365` - M365
* `github` - GitHub
* `iac` - IaC
explode: false
style: form
- in: query
@@ -2417,6 +2429,7 @@ paths:
- azure
- gcp
- github
- iac
- kubernetes
- m365
description: |-
@@ -2426,6 +2439,7 @@ paths:
* `kubernetes` - Kubernetes
* `m365` - M365
* `github` - GitHub
* `iac` - IaC
- in: query
name: filter[provider_type__in]
schema:
@@ -2449,6 +2463,7 @@ paths:
* `kubernetes` - Kubernetes
* `m365` - M365
* `github` - GitHub
* `iac` - IaC
explode: false
style: form
- in: query
@@ -3347,6 +3362,7 @@ paths:
- azure
- gcp
- github
- iac
- kubernetes
- m365
description: |-
@@ -3356,6 +3372,7 @@ paths:
* `kubernetes` - Kubernetes
* `m365` - M365
* `github` - GitHub
* `iac` - IaC
- in: query
name: filter[provider_type__in]
schema:
@@ -3379,6 +3396,7 @@ paths:
* `kubernetes` - Kubernetes
* `m365` - M365
* `github` - GitHub
* `iac` - IaC
explode: false
style: form
- in: query
@@ -3514,6 +3532,7 @@ paths:
- azure
- gcp
- github
- iac
- kubernetes
- m365
description: |-
@@ -3523,6 +3542,7 @@ paths:
* `kubernetes` - Kubernetes
* `m365` - M365
* `github` - GitHub
* `iac` - IaC
- in: query
name: filter[provider_type__in]
schema:
@@ -3546,6 +3566,7 @@ paths:
* `kubernetes` - Kubernetes
* `m365` - M365
* `github` - GitHub
* `iac` - IaC
explode: false
style: form
- in: query
@@ -3716,6 +3737,7 @@ paths:
- azure
- gcp
- github
- iac
- kubernetes
- m365
description: |-
@@ -3725,6 +3747,7 @@ paths:
* `kubernetes` - Kubernetes
* `m365` - M365
* `github` - GitHub
* `iac` - IaC
- in: query
name: filter[provider_type__in]
schema:
@@ -3748,6 +3771,7 @@ paths:
* `kubernetes` - Kubernetes
* `m365` - M365
* `github` - GitHub
* `iac` - IaC
explode: false
style: form
- in: query
@@ -4430,6 +4454,7 @@ paths:
- azure
- gcp
- github
- iac
- kubernetes
- m365
description: |-
@@ -4439,6 +4464,7 @@ paths:
* `kubernetes` - Kubernetes
* `m365` - M365
* `github` - GitHub
* `iac` - IaC
- in: query
name: filter[provider__in]
schema:
@@ -5044,6 +5070,7 @@ paths:
- azure
- gcp
- github
- iac
- kubernetes
- m365
description: |-
@@ -5053,6 +5080,7 @@ paths:
* `kubernetes` - Kubernetes
* `m365` - M365
* `github` - GitHub
* `iac` - IaC
- in: query
name: filter[provider_type__in]
schema:
@@ -5076,6 +5104,7 @@ paths:
* `kubernetes` - Kubernetes
* `m365` - M365
* `github` - GitHub
* `iac` - IaC
explode: false
style: form
- in: query
@@ -5409,6 +5438,7 @@ paths:
- azure
- gcp
- github
- iac
- kubernetes
- m365
description: |-
@@ -5418,6 +5448,7 @@ paths:
* `kubernetes` - Kubernetes
* `m365` - M365
* `github` - GitHub
* `iac` - IaC
- in: query
name: filter[provider_type__in]
schema:
@@ -5441,6 +5472,7 @@ paths:
* `kubernetes` - Kubernetes
* `m365` - M365
* `github` - GitHub
* `iac` - IaC
explode: false
style: form
- in: query
@@ -5675,6 +5707,7 @@ paths:
- azure
- gcp
- github
- iac
- kubernetes
- m365
description: |-
@@ -5684,6 +5717,7 @@ paths:
* `kubernetes` - Kubernetes
* `m365` - M365
* `github` - GitHub
* `iac` - IaC
- in: query
name: filter[provider_type__in]
schema:
@@ -5707,6 +5741,7 @@ paths:
* `kubernetes` - Kubernetes
* `m365` - M365
* `github` - GitHub
* `iac` - IaC
explode: false
style: form
- in: query
@@ -5947,6 +5982,7 @@ paths:
- azure
- gcp
- github
- iac
- kubernetes
- m365
description: |-
@@ -5956,6 +5992,7 @@ paths:
* `kubernetes` - Kubernetes
* `m365` - M365
* `github` - GitHub
* `iac` - IaC
- in: query
name: filter[provider_type__in]
schema:
@@ -5979,6 +6016,7 @@ paths:
* `kubernetes` - Kubernetes
* `m365` - M365
* `github` - GitHub
* `iac` - IaC
explode: false
style: form
- in: query
@@ -6779,6 +6817,7 @@ paths:
- azure
- gcp
- github
- iac
- kubernetes
- m365
description: |-
@@ -6788,6 +6827,7 @@ paths:
* `kubernetes` - Kubernetes
* `m365` - M365
* `github` - GitHub
* `iac` - IaC
- in: query
name: filter[provider_type__in]
schema:
@@ -6811,6 +6851,7 @@ paths:
* `kubernetes` - Kubernetes
* `m365` - M365
* `github` - GitHub
* `iac` - IaC
explode: false
style: form
- in: query
@@ -11767,6 +11808,17 @@ components:
required:
- github_app_id
- github_app_key
- type: object
title: IaC Repository Credentials
properties:
repository_url:
type: string
description: Repository URL to scan for IaC files.
access_token:
type: string
description: Optional access token for private repositories.
required:
- repository_url
writeOnly: true
required:
- secret
@@ -13540,6 +13592,17 @@ components:
required:
- github_app_id
- github_app_key
- type: object
title: IaC Repository Credentials
properties:
repository_url:
type: string
description: Repository URL to scan for IaC files.
access_token:
type: string
description: Optional access token for private repositories.
required:
- repository_url
writeOnly: true
required:
- secret_type
@@ -13788,6 +13851,17 @@ components:
required:
- github_app_id
- github_app_key
- type: object
title: IaC Repository Credentials
properties:
repository_url:
type: string
description: Repository URL to scan for IaC files.
access_token:
type: string
description: Optional access token for private repositories.
required:
- repository_url
writeOnly: true
required:
- secret_type
@@ -14052,6 +14126,17 @@ components:
required:
- github_app_id
- github_app_key
- type: object
title: IaC Repository Credentials
properties:
repository_url:
type: string
description: Repository URL to scan for IaC files.
access_token:
type: string
description: Optional access token for private repositories.
required:
- repository_url
writeOnly: true
required:
- secret

View File

@@ -991,6 +991,16 @@ class TestProviderViewSet:
"uid": "a12345678901234567890123456789012345678",
"alias": "Long Username",
},
{
"provider": "iac",
"uid": "https://github.com/user/repo.git",
"alias": "Git Repo",
},
{
"provider": "iac",
"uid": "https://gitlab.com/user/project",
"alias": "GitLab Repo",
},
]
),
)
@@ -1140,6 +1150,33 @@ class TestProviderViewSet:
"github-uid",
"uid",
),
(
{
"provider": "iac",
"uid": "not-a-url",
"alias": "test",
},
"iac-uid",
"uid",
),
(
{
"provider": "iac",
"uid": "ftp://invalid-protocol.com/repo",
"alias": "test",
},
"iac-uid",
"uid",
),
(
{
"provider": "iac",
"uid": "http://",
"alias": "test",
},
"iac-uid",
"uid",
),
]
),
)

View File

@@ -18,6 +18,7 @@ from prowler.providers.azure.azure_provider import AzureProvider
from prowler.providers.common.models import Connection
from prowler.providers.gcp.gcp_provider import GcpProvider
from prowler.providers.github.github_provider import GithubProvider
from prowler.providers.iac.iac_provider import IacProvider
from prowler.providers.kubernetes.kubernetes_provider import KubernetesProvider
from prowler.providers.m365.m365_provider import M365Provider
@@ -65,6 +66,7 @@ def return_prowler_provider(
| AzureProvider
| GcpProvider
| GithubProvider
| IacProvider
| KubernetesProvider
| M365Provider
]:
@@ -74,7 +76,7 @@ def return_prowler_provider(
provider (Provider): The provider object containing the provider type and associated secrets.
Returns:
AwsProvider | AzureProvider | GcpProvider | GithubProvider | KubernetesProvider | M365Provider: The corresponding provider class.
AwsProvider | AzureProvider | GcpProvider | GithubProvider | IacProvider | KubernetesProvider | M365Provider: The corresponding provider class.
Raises:
ValueError: If the provider type specified in `provider.provider` is not supported.
@@ -92,6 +94,8 @@ def return_prowler_provider(
prowler_provider = M365Provider
case Provider.ProviderChoices.GITHUB.value:
prowler_provider = GithubProvider
case Provider.ProviderChoices.IAC.value:
prowler_provider = IacProvider
case _:
raise ValueError(f"Provider type {provider.provider} not supported")
return prowler_provider
@@ -128,6 +132,16 @@ def get_prowler_provider_kwargs(
**prowler_provider_kwargs,
"organizations": [provider.uid],
}
elif provider.provider == Provider.ProviderChoices.IAC.value:
# For IaC provider, uid contains the repository URL
# Extract the access token if present in the secret
prowler_provider_kwargs = {
"scan_repository_url": provider.uid,
}
if "access_token" in provider.secret.secret:
prowler_provider_kwargs["oauth_app_token"] = provider.secret.secret[
"access_token"
]
if mutelist_processor:
mutelist_content = mutelist_processor.configuration.get("Mutelist", {})
@@ -145,6 +159,7 @@ def initialize_prowler_provider(
| AzureProvider
| GcpProvider
| GithubProvider
| IacProvider
| KubernetesProvider
| M365Provider
):
@@ -155,8 +170,8 @@ def initialize_prowler_provider(
mutelist_processor (Processor): The mutelist processor object containing the mutelist configuration.
Returns:
AwsProvider | AzureProvider | GcpProvider | GithubProvider | KubernetesProvider | M365Provider: An instance of the corresponding provider class
(`AwsProvider`, `AzureProvider`, `GcpProvider`, `GithubProvider`, `KubernetesProvider` or `M365Provider`) initialized with the
AwsProvider | AzureProvider | GcpProvider | GithubProvider | IacProvider | KubernetesProvider | M365Provider: An instance of the corresponding provider class
(`AwsProvider`, `AzureProvider`, `GcpProvider`, `GithubProvider`, `IacProvider`, `KubernetesProvider` or `M365Provider`) initialized with the
provider's secrets.
"""
prowler_provider = return_prowler_provider(provider)
@@ -180,8 +195,22 @@ def prowler_provider_connection_test(provider: Provider) -> Connection:
except Provider.secret.RelatedObjectDoesNotExist as secret_error:
return Connection(is_connected=False, error=secret_error)
# For IaC provider, construct the kwargs properly for test_connection
if provider.provider == Provider.ProviderChoices.IAC.value:
# Don't pass repository_url from secret, use scan_repository_url with the UID
iac_test_kwargs = {
"scan_repository_url": provider.uid,
"raise_on_exception": False,
}
# Add access_token if present in the secret
if "access_token" in prowler_provider_kwargs:
iac_test_kwargs["access_token"] = prowler_provider_kwargs["access_token"]
return prowler_provider.test_connection(**iac_test_kwargs)
else:
return prowler_provider.test_connection(
**prowler_provider_kwargs, provider_id=provider.uid, raise_on_exception=False
**prowler_provider_kwargs,
provider_id=provider.uid,
raise_on_exception=False,
)

View File

@@ -213,6 +213,21 @@ from rest_framework_json_api import serializers
},
"required": ["github_app_id", "github_app_key"],
},
{
"type": "object",
"title": "IaC Repository Credentials",
"properties": {
"repository_url": {
"type": "string",
"description": "Repository URL to scan for IaC files.",
},
"access_token": {
"type": "string",
"description": "Optional access token for private repositories.",
},
},
"required": ["repository_url"],
},
]
}
)

View File

@@ -1223,6 +1223,8 @@ class BaseWriteProviderSecretSerializer(BaseWriteSerializer):
serializer = GCPProviderSecret(data=secret)
elif provider_type == Provider.ProviderChoices.GITHUB.value:
serializer = GithubProviderSecret(data=secret)
elif provider_type == Provider.ProviderChoices.IAC.value:
serializer = IacProviderSecret(data=secret)
elif provider_type == Provider.ProviderChoices.KUBERNETES.value:
serializer = KubernetesProviderSecret(data=secret)
elif provider_type == Provider.ProviderChoices.M365.value:
@@ -1312,6 +1314,14 @@ class GithubProviderSecret(serializers.Serializer):
resource_name = "provider-secrets"
class IacProviderSecret(serializers.Serializer):
repository_url = serializers.CharField()
access_token = serializers.CharField(required=False)
class Meta:
resource_name = "provider-secrets"
class AWSRoleAssumptionProviderSecret(serializers.Serializer):
role_arn = serializers.CharField()
external_id = serializers.CharField()

View File

@@ -100,6 +100,10 @@ COMPLIANCE_CLASS_MAP = {
"github": [
(lambda name: name.startswith("cis_"), GithubCIS),
],
"iac": [
# IaC provider doesn't have specific compliance frameworks yet
# Trivy handles its own compliance checks
],
}

View File

@@ -309,10 +309,17 @@ class Finding(BaseModel):
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_line_range
output_data["resource_line_range"] = check_output.resource_line_range
output_data["resource_name"] = getattr(
check_output, "resource_name", ""
)
output_data["resource_uid"] = getattr(check_output, "resource_name", "")
# For IaC, resource_line_range only exists on CheckReportIAC, not on Finding objects
output_data["region"] = getattr(
check_output, "resource_line_range", "file"
)
output_data["resource_line_range"] = getattr(
check_output, "resource_line_range", ""
)
output_data["framework"] = check_output.check_metadata.ServiceName
# check_output Unique ID
@@ -385,6 +392,10 @@ class Finding(BaseModel):
finding.subscription = list(provider.identity.subscriptions.keys())[0]
elif provider.type == "gcp":
finding.project_id = list(provider.projects.keys())[0]
elif provider.type == "iac":
# For IaC, we don't have resource_line_range in the Finding model
# It would need to be extracted from the resource metadata if needed
finding.resource_line_range = "" # Set empty for compatibility
finding.check_metadata = CheckMetadata(
Provider=finding.check_metadata["provider"],

View File

@@ -93,6 +93,16 @@ class Scan:
# Load bulk compliance frameworks
self._bulk_compliance_frameworks = Compliance.get_bulk(provider.type)
# Special setup for IaC provider - override inputs to work with traditional flow
if provider.type == "iac":
# IaC doesn't use traditional Prowler checks, so clear all input parameters
# to avoid validation errors and let it flow through the normal logic
checks = None
services = None
excluded_checks = None
excluded_services = None
self._bulk_checks_metadata = {}
else:
# Get bulk checks metadata for the provider
self._bulk_checks_metadata = CheckMetadata.get_bulk(provider.type)
# Complete checks metadata with the compliance framework specification
@@ -148,6 +158,9 @@ class Scan:
)
# Load checks to execute
if provider.type == "iac":
self._checks_to_execute = ["iac_scan"] # Dummy check name for IaC
else:
self._checks_to_execute = sorted(
load_checks_to_execute(
bulk_checks_metadata=self._bulk_checks_metadata,
@@ -184,6 +197,10 @@ class Scan:
self._number_of_checks_to_execute = len(self._checks_to_execute)
# Set up service-based checks tracking
if provider.type == "iac":
service_checks_to_execute = {"iac": set(["iac_scan"])}
else:
service_checks_to_execute = get_service_checks_to_execute(
self._checks_to_execute
)
@@ -245,6 +262,9 @@ class Scan:
Exception: If any other error occurs during the execution of a check.
"""
try:
# Initialize check_name for error handling
check_name = None
# Using SimpleNamespace to create a mocked object
arguments = SimpleNamespace()
@@ -266,6 +286,65 @@ class Scan:
start_time = datetime.datetime.now()
# Special handling for IaC provider
if self._provider.type == "iac":
# IaC provider doesn't use regular checks, it runs Trivy directly
from prowler.providers.iac.iac_provider import IacProvider
if isinstance(self._provider, IacProvider):
logger.info("Running IaC scan with Trivy...")
# Run the IaC scan
iac_reports = self._provider.run()
# Convert IaC reports to Finding objects
findings = []
from datetime import timezone
from prowler.lib.outputs.common import Status
from prowler.lib.outputs.finding import Finding
for report in iac_reports:
# Generate unique UID for the finding
finding_uid = f"{report.check_metadata.CheckID}-{report.resource_name}-{report.resource_line_range}"
# Convert status string to Status enum
status_enum = (
Status.FAIL if report.status == "FAIL" else Status.PASS
)
if report.muted:
status_enum = Status.MUTED
finding = Finding(
auth_method="Repository", # IaC uses repository as auth method
timestamp=datetime.datetime.now(timezone.utc),
account_uid=self._provider.scan_repository_url or "local",
account_name="IaC Repository",
metadata=report.check_metadata, # Pass the CheckMetadata object directly
uid=finding_uid,
status=status_enum,
status_extended=report.status_extended,
muted=report.muted,
resource_uid=report.resource_name, # For IaC, the file path is the UID
resource_metadata=report.resource, # The raw finding dict
resource_name=report.resource_name,
resource_details=report.resource_details,
resource_tags={}, # IaC doesn't have resource tags
region="global", # IaC doesn't have regions
compliance={}, # IaC doesn't have compliance mappings yet
raw=report.resource, # The raw finding dict
)
findings.append(finding)
# Update progress and yield findings
self._number_of_checks_completed = 1
self._number_of_checks_to_execute = 1
yield (100.0, findings)
# Calculate duration
end_time = datetime.datetime.now()
self._duration = int((end_time - start_time).total_seconds())
return
for check_name in checks_to_execute:
try:
# Recover service from check name
@@ -341,9 +420,14 @@ class Scan:
# Update the scan duration when all checks are completed
self._duration = int((datetime.datetime.now() - start_time).total_seconds())
except Exception as error:
if check_name:
logger.error(
f"{check_name} - {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
)
else:
logger.error(
f"Scan error - {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
)
def get_completed_services(self) -> set[str]:
"""

View File

@@ -242,6 +242,12 @@ class IacProvider(Provider):
logger.info(
f"Cloning repository {original_url} into {temporary_directory}..."
)
# Check if we're in an environment with a TTY
# Celery workers and other non-interactive environments don't have TTY
# and cannot use the alive_bar
try:
if sys.stdout.isatty():
with alive_bar(
ctrl_c=False,
bar="blocks",
@@ -256,6 +262,17 @@ class IacProvider(Provider):
except Exception as clone_error:
bar.title = "-> Cloning failed!"
raise clone_error
else:
# No TTY, just clone without progress bar
logger.info(f"Cloning {original_url}...")
porcelain.clone(repository_url, temporary_directory, depth=1)
logger.info("Repository cloned successfully!")
except (AttributeError, OSError):
# Fallback if isatty() check fails
logger.info(f"Cloning {original_url}...")
porcelain.clone(repository_url, temporary_directory, depth=1)
logger.info("Repository cloned successfully!")
return temporary_directory
except Exception as error:
logger.critical(
@@ -302,6 +319,10 @@ class IacProvider(Provider):
]
if exclude_path:
trivy_command.extend(["--skip-dirs", ",".join(exclude_path)])
# Check if we're in an environment with a TTY
try:
if sys.stdout.isatty():
with alive_bar(
ctrl_c=False,
bar="blocks",
@@ -321,6 +342,24 @@ class IacProvider(Provider):
except Exception as error:
bar.title = "-> Scan failed!"
raise error
else:
# No TTY, just run without progress bar
logger.info(f"Running Trivy scan on {directory}...")
process = subprocess.run(
trivy_command,
capture_output=True,
text=True,
)
logger.info("Trivy scan completed!")
except (AttributeError, OSError):
# Fallback if isatty() check fails
logger.info(f"Running Trivy scan on {directory}...")
process = subprocess.run(
trivy_command,
capture_output=True,
text=True,
)
logger.info("Trivy scan completed!")
# Log Trivy's stderr output with preserved log levels
if process.stderr:
for line in process.stderr.strip().split("\n"):
@@ -434,3 +473,99 @@ class IacProvider(Provider):
)
print_boxes(report_lines, report_title)
@staticmethod
def test_connection(
scan_repository_url: str = None,
oauth_app_token: str = None,
access_token: str = None,
raise_on_exception: bool = True,
provider_id: str = None,
) -> "Connection":
"""Test connection to IaC repository.
Test the connection to the IaC repository using the provided credentials.
Args:
scan_repository_url (str): Repository URL to scan.
oauth_app_token (str): OAuth App token for authentication.
access_token (str): Access token for authentication (alias for oauth_app_token).
raise_on_exception (bool): Flag indicating whether to raise an exception if the connection fails.
provider_id (str): The provider ID, in this case it's the repository URL.
Returns:
Connection: Connection object with success status or error information.
Raises:
Exception: If failed to test the connection to the repository.
Examples:
>>> IacProvider.test_connection(scan_repository_url="https://github.com/user/repo")
Connection(is_connected=True)
"""
from prowler.providers.common.models import Connection
try:
# If provider_id is provided and scan_repository_url is not, use provider_id as the repository URL
if provider_id and not scan_repository_url:
scan_repository_url = provider_id
# Handle both oauth_app_token and access_token parameters
if access_token and not oauth_app_token:
oauth_app_token = access_token
if not scan_repository_url:
return Connection(
is_connected=False,
error="Repository URL is required"
)
# Try to clone the repository to test the connection
with tempfile.TemporaryDirectory() as temp_dir:
try:
if oauth_app_token:
# If token is provided, use it for authentication
# Extract the domain and path from the URL
import re
url_pattern = r"(https?://)([^/]+)/(.+)"
match = re.match(url_pattern, scan_repository_url)
if match:
protocol, domain, path = match.groups()
# Construct URL with token
auth_url = f"{protocol}x-access-token:{oauth_app_token}@{domain}/{path}"
else:
auth_url = scan_repository_url
else:
# Public repository
auth_url = scan_repository_url
# Use dulwich to test the connection
porcelain.ls_remote(auth_url)
return Connection(is_connected=True)
except Exception as e:
error_msg = str(e)
if "authentication" in error_msg.lower() or "401" in error_msg:
return Connection(
is_connected=False,
error="Authentication failed. Please check your access token."
)
elif "404" in error_msg or "not found" in error_msg.lower():
return Connection(
is_connected=False,
error="Repository not found or not accessible."
)
else:
return Connection(
is_connected=False,
error=f"Failed to connect to repository: {error_msg}"
)
except Exception as error:
if raise_on_exception:
raise
return Connection(
is_connected=False,
error=f"Unexpected error testing connection: {str(error)}"
)

View File

@@ -2,6 +2,14 @@
All notable changes to the **Prowler UI** are documented in this file.
## [Unreleased]
### 🚀 Added
- IaC (Infrastructure as Code) provider support for scanning remote repositories [(#TBD)](https://github.com/prowler-cloud/prowler/pull/TBD)
---
## [1.12.1] (Prowler v5.12.1)
### 🚀 Added
@@ -14,6 +22,8 @@ All notable changes to the **Prowler UI** are documented in this file.
- Field-level email validation message [(#8698)] (https://github.com/prowler-cloud/prowler/pull/8698)
- POST method on auth form [(#8699)] (https://github.com/prowler-cloud/prowler/pull/8699)
---
## [1.12.0] (Prowler v5.12.0)
### 🚀 Added

View File

@@ -5,6 +5,7 @@ import {
AzureProviderBadge,
GCPProviderBadge,
GitHubProviderBadge,
IacProviderBadge,
KS8ProviderBadge,
M365ProviderBadge,
} from "../icons/providers-badge";
@@ -62,3 +63,12 @@ export const CustomProviderInputGitHub = () => {
</div>
);
};
export const CustomProviderInputIac = () => {
return (
<div className="flex items-center gap-x-2">
<IacProviderBadge width={25} height={25} />
<p className="text-sm">Infrastructure as Code</p>
</div>
);
};

View File

@@ -11,6 +11,7 @@ import {
CustomProviderInputAzure,
CustomProviderInputGCP,
CustomProviderInputGitHub,
CustomProviderInputIac,
CustomProviderInputKubernetes,
CustomProviderInputM365,
} from "./custom-provider-inputs";
@@ -43,6 +44,10 @@ const providerDisplayData: Record<
label: "GitHub",
component: <CustomProviderInputGitHub />,
},
iac: {
label: "Infrastructure as Code",
component: <CustomProviderInputIac />,
},
};
const dataInputsProvider = PROVIDER_TYPES.map((providerType) => ({

View File

@@ -0,0 +1,44 @@
import * as React from "react";
import { IconSvgProps } from "@/types";
export const IacProviderBadge: React.FC<IconSvgProps> = ({
size,
width,
height,
...props
}) => (
<svg
xmlns="http://www.w3.org/2000/svg"
aria-hidden="true"
fill="none"
focusable="false"
height={size || height}
role="presentation"
viewBox="0 0 24 24"
width={size || width}
{...props}
>
<path
d="M13 21L17 3"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="M7 8L3 12L7 16"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="M17 8L21 12L17 16"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
);

View File

@@ -2,5 +2,6 @@ export * from "./aws-provider-badge";
export * from "./azure-provider-badge";
export * from "./gcp-provider-badge";
export * from "./github-provider-badge";
export * from "./iac-provider-badge";
export * from "./ks8-provider-badge";
export * from "./m365-provider-badge";

View File

@@ -49,6 +49,7 @@ export const ProvidersOverview = ({
gcp: "GCP",
kubernetes: "Kubernetes",
github: "GitHub",
iac: "IaC",
};
const providers = PROVIDER_TYPES.map((providerType) => ({

View File

@@ -15,6 +15,7 @@ const providerTypeLabels: Record<ProviderType, string> = {
m365: "Microsoft 365",
kubernetes: "Kubernetes",
github: "GitHub",
iac: "Infrastructure as Code",
};
interface EnhancedProviderSelectorProps {

View File

@@ -12,6 +12,7 @@ import {
AzureProviderBadge,
GCPProviderBadge,
GitHubProviderBadge,
IacProviderBadge,
KS8ProviderBadge,
M365ProviderBadge,
} from "../icons/providers-badge";
@@ -78,6 +79,12 @@ export const RadioGroupProvider: React.FC<RadioGroupProviderProps> = ({
<span className="ml-2">GitHub</span>
</div>
</CustomRadio>
<CustomRadio description="Infrastructure as Code" value="iac">
<div className="flex items-center">
<IacProviderBadge size={26} />
<span className="ml-2">Infrastructure as Code</span>
</div>
</CustomRadio>
</div>
</RadioGroup>
{errorMessage && (

View File

@@ -16,6 +16,7 @@ import {
AzureCredentials,
GCPDefaultCredentials,
GCPServiceAccountKey,
IacCredentials,
KubernetesCredentials,
M365Credentials,
ProviderType,
@@ -28,6 +29,7 @@ import { GCPDefaultCredentialsForm } from "./select-credentials-type/gcp/credent
import { GCPServiceAccountKeyForm } from "./select-credentials-type/gcp/credentials-type/gcp-service-account-key-form";
import { AzureCredentialsForm } from "./via-credentials/azure-credentials-form";
import { GitHubCredentialsForm } from "./via-credentials/github-credentials-form";
import { IacCredentialsForm } from "./via-credentials/iac-credentials-form";
import { KubernetesCredentialsForm } from "./via-credentials/k8s-credentials-form";
import { M365CredentialsForm } from "./via-credentials/m365-credentials-form";
@@ -133,6 +135,11 @@ export const BaseCredentialsForm = ({
credentialsType={searchParamsObj.get("via") || undefined}
/>
)}
{providerType === "iac" && (
<IacCredentialsForm
control={form.control as unknown as Control<IacCredentials>}
/>
)}
<div className="flex w-full justify-end sm:space-x-6">
{showBackButton && requiresBackButton(searchParamsObj.get("via")) && (

View File

@@ -51,6 +51,11 @@ const getProviderFieldDetails = (providerType?: ProviderType) => {
label: "Username",
placeholder: "e.g. your-github-username",
};
case "iac":
return {
label: "Repository URL",
placeholder: "e.g. https://github.com/user/repo",
};
default:
return {
label: "Provider UID",

View File

@@ -0,0 +1,44 @@
import { Control } from "react-hook-form";
import { CustomInput } from "@/components/ui/custom";
import { IacCredentials } from "@/types";
export const IacCredentialsForm = ({
control,
}: {
control: Control<IacCredentials>;
}) => {
return (
<>
<div className="flex flex-col">
<div className="text-md font-bold leading-9 text-default-foreground">
Connect via Repository
</div>
<div className="text-sm text-default-500">
Please provide the repository URL to scan for Infrastructure as Code
files.
</div>
</div>
<CustomInput
control={control}
name="repository_url"
label="Repository URL"
labelPlacement="inside"
placeholder="https://github.com/user/repo or https://github.com/user/repo.git"
variant="bordered"
isRequired
isInvalid={!!control._formState.errors.repository_url}
/>
<CustomInput
control={control}
name="access_token"
label="Access Token (Optional)"
labelPlacement="inside"
placeholder="Token for private repositories (optional)"
variant="bordered"
type="password"
isInvalid={!!control._formState.errors.access_token}
/>
</>
);
};

View File

@@ -1,4 +1,5 @@
export * from "./azure-credentials-form";
export * from "./github-credentials-form";
export * from "./iac-credentials-form";
export * from "./k8s-credentials-form";
export * from "./m365-credentials-form";

View File

@@ -5,6 +5,7 @@ import {
AzureProviderBadge,
GCPProviderBadge,
GitHubProviderBadge,
IacProviderBadge,
KS8ProviderBadge,
M365ProviderBadge,
} from "@/components/icons/providers-badge";
@@ -24,6 +25,8 @@ export const getProviderLogo = (provider: ProviderType) => {
return <M365ProviderBadge width={35} height={35} />;
case "github":
return <GitHubProviderBadge width={35} height={35} />;
case "iac":
return <IacProviderBadge width={35} height={35} />;
default:
return null;
}
@@ -43,6 +46,8 @@ export const getProviderName = (provider: ProviderType): string => {
return "Microsoft 365";
case "github":
return "GitHub";
case "iac":
return "Infrastructure as Code";
default:
return "Unknown Provider";
}

View File

@@ -32,6 +32,11 @@ export const getProviderHelpText = (provider: string) => {
text: "Need help connecting your GitHub account?",
link: "https://goto.prowler.com/provider-github",
};
case "iac":
return {
text: "Need help scanning your Infrastructure as Code repository?",
link: "https://goto.prowler.com/provider-iac",
};
default:
return {
text: "How to setup a provider?",

View File

@@ -190,6 +190,20 @@ export const buildGitHubSecret = (formData: FormData) => {
return {};
};
export const buildIacSecret = (formData: FormData) => {
const secret = {
[ProviderCredentialFields.REPOSITORY_URL]: getFormValue(
formData,
ProviderCredentialFields.REPOSITORY_URL,
),
[ProviderCredentialFields.ACCESS_TOKEN]: getFormValue(
formData,
ProviderCredentialFields.ACCESS_TOKEN,
),
};
return filterEmptyValues(secret);
};
// Main function to build secret configuration
export const buildSecretConfig = (
formData: FormData,
@@ -224,6 +238,10 @@ export const buildSecretConfig = (
secretType: "static",
secret: buildGitHubSecret(formData),
}),
iac: () => ({
secretType: "static",
secret: buildIacSecret(formData),
}),
};
const builder = secretBuilders[providerType];

View File

@@ -42,6 +42,10 @@ export const ProviderCredentialFields = {
OAUTH_APP_TOKEN: "oauth_app_token",
GITHUB_APP_ID: "github_app_id",
GITHUB_APP_KEY: "github_app_key_content",
// IaC fields
REPOSITORY_URL: "repository_url",
ACCESS_TOKEN: "access_token",
} as const;
// Type for credential field values
@@ -70,6 +74,8 @@ export const ErrorPointers = {
OAUTH_APP_TOKEN: "/data/attributes/secret/oauth_app_token",
GITHUB_APP_ID: "/data/attributes/secret/github_app_id",
GITHUB_APP_KEY: "/data/attributes/secret/github_app_key_content",
REPOSITORY_URL: "/data/attributes/secret/repository_url",
ACCESS_TOKEN: "/data/attributes/secret/access_token",
} as const;
export type ErrorPointer = (typeof ErrorPointers)[keyof typeof ErrorPointers];

View File

@@ -239,12 +239,19 @@ export type KubernetesCredentials = {
[ProviderCredentialFields.PROVIDER_ID]: string;
};
export type IacCredentials = {
[ProviderCredentialFields.REPOSITORY_URL]: string;
[ProviderCredentialFields.ACCESS_TOKEN]?: string;
[ProviderCredentialFields.PROVIDER_ID]: string;
};
export type CredentialsFormSchema =
| AWSCredentials
| AzureCredentials
| GCPDefaultCredentials
| GCPServiceAccountKey
| KubernetesCredentials
| IacCredentials
| M365Credentials;
export interface SearchParamsProps {

View File

@@ -110,6 +110,11 @@ export const addProviderFormSchema = z
[ProviderCredentialFields.PROVIDER_ALIAS]: z.string(),
providerUid: z.string(),
}),
z.object({
providerType: z.literal("iac"),
[ProviderCredentialFields.PROVIDER_ALIAS]: z.string(),
providerUid: z.string(),
}),
]),
);
@@ -190,6 +195,15 @@ export const addCredentialsFormSchema = (
.string()
.optional(),
}
: providerType === "iac"
? {
[ProviderCredentialFields.REPOSITORY_URL]: z
.string()
.nonempty("Repository URL is required"),
[ProviderCredentialFields.ACCESS_TOKEN]: z
.string()
.optional(),
}
: {}),
})
.superRefine((data: Record<string, any>, ctx) => {

View File

@@ -5,6 +5,7 @@ export const PROVIDER_TYPES = [
"kubernetes",
"m365",
"github",
"iac",
] as const;
export type ProviderType = (typeof PROVIDER_TYPES)[number];