wip: add IaC to API

This commit is contained in:
Andoni A.
2025-09-09 16:54:39 +02:00
parent ff22d198d0
commit dbff60576b
11 changed files with 269 additions and 11 deletions
+7
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
+20
View File
@@ -19,6 +19,7 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
libtool \
libxslt1-dev \
python3-dev \
ca-certificates \
&& rm -rf /var/lib/apt/lists/*
# Install PowerShell
@@ -36,6 +37,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
@@ -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,
),
]
+14
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)
+85
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
+37
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",
),
]
),
)
+37 -6
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,17 @@ 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
# TODO: review this
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 +160,7 @@ def initialize_prowler_provider(
| AzureProvider
| GcpProvider
| GithubProvider
| IacProvider
| KubernetesProvider
| M365Provider
):
@@ -155,8 +171,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,9 +196,24 @@ def prowler_provider_connection_test(provider: Provider) -> Connection:
except Provider.secret.RelatedObjectDoesNotExist as secret_error:
return Connection(is_connected=False, error=secret_error)
return prowler_provider.test_connection(
**prowler_provider_kwargs, provider_id=provider.uid, raise_on_exception=False
)
# For IaC provider, construct the kwargs properly for test_connection
# TODO: review this
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,
)
def prowler_integration_connection_test(integration: Integration) -> Connection:
@@ -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"],
},
]
}
)
+10
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()
+4
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
],
}
+6 -5
View File
@@ -260,7 +260,7 @@ class Scan:
try:
# Initialize check_name for error handling
check_name = None
# Using SimpleNamespace to create a mocked object
arguments = SimpleNamespace()
@@ -286,17 +286,18 @@ class Scan:
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 = []
for report in iac_reports:
# Extract metadata from CheckReportIAC
check_metadata = report.check_metadata
finding = Finding(
provider=self._provider.type,
check_id=check_metadata.CheckID,
@@ -320,12 +321,12 @@ class Scan:
compliance={}, # IaC doesn't have compliance mappings yet
)
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())