feat(iac): add IaC to Prowler App (#8751)

This commit is contained in:
Andoni Alonso
2025-11-04 10:01:58 +01:00
committed by GitHub
parent 61a66f2bbf
commit 601495166c
40 changed files with 903 additions and 128 deletions
+1
View File
@@ -5,6 +5,7 @@ All notable changes to the **Prowler API** are documented in this file.
## [1.15.0] (Prowler UNRELEASED)
### Added
- IaC (Infrastructure as Code) provider support for remote repositories [(#8751)](https://github.com/prowler-cloud/prowler/pull/8751)
- Extend `GET /api/v1/providers` with provider-type filters and optional pagination disable to support the new Overview filters [(#8975)](https://github.com/prowler-cloud/prowler/pull/8975)
- New endpoint to retrieve the number of providers grouped by provider type [(#8975)](https://github.com/prowler-cloud/prowler/pull/8975)
- Support for configuring multiple LLM providers [(#8772)](https://github.com/prowler-cloud/prowler/pull/8772)
+21
View File
@@ -5,6 +5,9 @@ LABEL maintainer="https://github.com/prowler-cloud/api"
ARG POWERSHELL_VERSION=7.5.0
ENV POWERSHELL_VERSION=${POWERSHELL_VERSION}
ARG TRIVY_VERSION=0.66.0
ENV TRIVY_VERSION=${TRIVY_VERSION}
# hadolint ignore=DL3008
RUN apt-get update && apt-get install -y --no-install-recommends \
wget \
@@ -36,6 +39,24 @@ RUN ARCH=$(uname -m) && \
ln -s /opt/microsoft/powershell/7/pwsh /usr/bin/pwsh && \
rm /tmp/powershell.tar.gz
# Install Trivy for IaC scanning
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,35 @@
# 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", "0053_lighthouse_bedrock_openai_compatible"),
]
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"),
("oci", "Oracle Cloud Infrastructure"),
("iac", "IaC"),
],
default="aws",
),
),
migrations.RunSQL(
"ALTER TYPE provider ADD VALUE IF NOT EXISTS 'iac';",
reverse_sql=migrations.RunSQL.noop,
),
]
+14
View File
@@ -284,6 +284,7 @@ class Provider(RowLevelSecurityProtectedModel):
KUBERNETES = "kubernetes", _("Kubernetes")
M365 = "m365", _("M365")
GITHUB = "github", _("GitHub")
IAC = "iac", _("IaC")
OCI = "oci", _("Oracle Cloud Infrastructure")
@staticmethod
@@ -355,6 +356,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",
)
@staticmethod
def validate_oci_uid(value):
if not re.match(
+85
View File
@@ -875,6 +875,7 @@ paths:
- azure
- gcp
- github
- iac
- kubernetes
- m365
- oci
@@ -885,6 +886,7 @@ paths:
* `kubernetes` - Kubernetes
* `m365` - M365
* `github` - GitHub
* `iac` - IaC
* `oci` - Oracle Cloud Infrastructure
- in: query
name: filter[provider_type__in]
@@ -910,6 +912,7 @@ paths:
* `kubernetes` - Kubernetes
* `m365` - M365
* `github` - GitHub
* `iac` - IaC
* `oci` - Oracle Cloud Infrastructure
explode: false
style: form
@@ -1406,6 +1409,7 @@ paths:
- azure
- gcp
- github
- iac
- kubernetes
- m365
- oci
@@ -1416,6 +1420,7 @@ paths:
* `kubernetes` - Kubernetes
* `m365` - M365
* `github` - GitHub
* `iac` - IaC
* `oci` - Oracle Cloud Infrastructure
- in: query
name: filter[provider_type__in]
@@ -1441,6 +1446,7 @@ paths:
* `kubernetes` - Kubernetes
* `m365` - M365
* `github` - GitHub
* `iac` - IaC
* `oci` - Oracle Cloud Infrastructure
explode: false
style: form
@@ -1845,6 +1851,7 @@ paths:
- azure
- gcp
- github
- iac
- kubernetes
- m365
- oci
@@ -1855,6 +1862,7 @@ paths:
* `kubernetes` - Kubernetes
* `m365` - M365
* `github` - GitHub
* `iac` - IaC
* `oci` - Oracle Cloud Infrastructure
- in: query
name: filter[provider_type__in]
@@ -1880,6 +1888,7 @@ paths:
* `kubernetes` - Kubernetes
* `m365` - M365
* `github` - GitHub
* `iac` - IaC
* `oci` - Oracle Cloud Infrastructure
explode: false
style: form
@@ -2282,6 +2291,7 @@ paths:
- azure
- gcp
- github
- iac
- kubernetes
- m365
- oci
@@ -2292,6 +2302,7 @@ paths:
* `kubernetes` - Kubernetes
* `m365` - M365
* `github` - GitHub
* `iac` - IaC
* `oci` - Oracle Cloud Infrastructure
- in: query
name: filter[provider_type__in]
@@ -2317,6 +2328,7 @@ paths:
* `kubernetes` - Kubernetes
* `m365` - M365
* `github` - GitHub
* `iac` - IaC
* `oci` - Oracle Cloud Infrastructure
explode: false
style: form
@@ -2707,6 +2719,7 @@ paths:
- azure
- gcp
- github
- iac
- kubernetes
- m365
- oci
@@ -2717,6 +2730,7 @@ paths:
* `kubernetes` - Kubernetes
* `m365` - M365
* `github` - GitHub
* `iac` - IaC
* `oci` - Oracle Cloud Infrastructure
- in: query
name: filter[provider_type__in]
@@ -2742,6 +2756,7 @@ paths:
* `kubernetes` - Kubernetes
* `m365` - M365
* `github` - GitHub
* `iac` - IaC
* `oci` - Oracle Cloud Infrastructure
explode: false
style: form
@@ -4536,6 +4551,7 @@ paths:
- azure
- gcp
- github
- iac
- kubernetes
- m365
- oci
@@ -4546,6 +4562,7 @@ paths:
* `kubernetes` - Kubernetes
* `m365` - M365
* `github` - GitHub
* `iac` - IaC
* `oci` - Oracle Cloud Infrastructure
- in: query
name: filter[provider_type__in]
@@ -4571,6 +4588,7 @@ paths:
* `kubernetes` - Kubernetes
* `m365` - M365
* `github` - GitHub
* `iac` - IaC
* `oci` - Oracle Cloud Infrastructure
explode: false
style: form
@@ -4717,6 +4735,7 @@ paths:
- azure
- gcp
- github
- iac
- kubernetes
- m365
- oci
@@ -4727,6 +4746,7 @@ paths:
* `kubernetes` - Kubernetes
* `m365` - M365
* `github` - GitHub
* `iac` - IaC
* `oci` - Oracle Cloud Infrastructure
- in: query
name: filter[provider_type__in]
@@ -4752,6 +4772,7 @@ paths:
* `kubernetes` - Kubernetes
* `m365` - M365
* `github` - GitHub
* `iac` - IaC
* `oci` - Oracle Cloud Infrastructure
explode: false
style: form
@@ -4964,6 +4985,7 @@ paths:
- azure
- gcp
- github
- iac
- kubernetes
- m365
- oci
@@ -4974,6 +4996,7 @@ paths:
* `kubernetes` - Kubernetes
* `m365` - M365
* `github` - GitHub
* `iac` - IaC
* `oci` - Oracle Cloud Infrastructure
- in: query
name: filter[provider_type__in]
@@ -4999,6 +5022,7 @@ paths:
* `kubernetes` - Kubernetes
* `m365` - M365
* `github` - GitHub
* `iac` - IaC
* `oci` - Oracle Cloud Infrastructure
explode: false
style: form
@@ -5691,6 +5715,7 @@ paths:
- azure
- gcp
- github
- iac
- kubernetes
- m365
- oci
@@ -5701,6 +5726,7 @@ paths:
* `kubernetes` - Kubernetes
* `m365` - M365
* `github` - GitHub
* `iac` - IaC
* `oci` - Oracle Cloud Infrastructure
- in: query
name: filter[provider__in]
@@ -6378,6 +6404,7 @@ paths:
- azure
- gcp
- github
- iac
- kubernetes
- m365
- oci
@@ -6388,6 +6415,7 @@ paths:
* `kubernetes` - Kubernetes
* `m365` - M365
* `github` - GitHub
* `iac` - IaC
* `oci` - Oracle Cloud Infrastructure
- in: query
name: filter[provider_type__in]
@@ -6413,6 +6441,7 @@ paths:
* `kubernetes` - Kubernetes
* `m365` - M365
* `github` - GitHub
* `iac` - IaC
* `oci` - Oracle Cloud Infrastructure
explode: false
style: form
@@ -6747,6 +6776,7 @@ paths:
- azure
- gcp
- github
- iac
- kubernetes
- m365
- oci
@@ -6757,6 +6787,7 @@ paths:
* `kubernetes` - Kubernetes
* `m365` - M365
* `github` - GitHub
* `iac` - IaC
* `oci` - Oracle Cloud Infrastructure
- in: query
name: filter[provider_type__in]
@@ -6782,6 +6813,7 @@ paths:
* `kubernetes` - Kubernetes
* `m365` - M365
* `github` - GitHub
* `iac` - IaC
* `oci` - Oracle Cloud Infrastructure
explode: false
style: form
@@ -7017,6 +7049,7 @@ paths:
- azure
- gcp
- github
- iac
- kubernetes
- m365
- oci
@@ -7027,6 +7060,7 @@ paths:
* `kubernetes` - Kubernetes
* `m365` - M365
* `github` - GitHub
* `iac` - IaC
* `oci` - Oracle Cloud Infrastructure
- in: query
name: filter[provider_type__in]
@@ -7052,6 +7086,7 @@ paths:
* `kubernetes` - Kubernetes
* `m365` - M365
* `github` - GitHub
* `iac` - IaC
* `oci` - Oracle Cloud Infrastructure
explode: false
style: form
@@ -7293,6 +7328,7 @@ paths:
- azure
- gcp
- github
- iac
- kubernetes
- m365
- oci
@@ -7303,6 +7339,7 @@ paths:
* `kubernetes` - Kubernetes
* `m365` - M365
* `github` - GitHub
* `iac` - IaC
* `oci` - Oracle Cloud Infrastructure
- in: query
name: filter[provider_type__in]
@@ -7328,6 +7365,7 @@ paths:
* `kubernetes` - Kubernetes
* `m365` - M365
* `github` - GitHub
* `iac` - IaC
* `oci` - Oracle Cloud Infrastructure
explode: false
style: form
@@ -8132,6 +8170,7 @@ paths:
- azure
- gcp
- github
- iac
- kubernetes
- m365
- oci
@@ -8142,6 +8181,7 @@ paths:
* `kubernetes` - Kubernetes
* `m365` - M365
* `github` - GitHub
* `iac` - IaC
* `oci` - Oracle Cloud Infrastructure
- in: query
name: filter[provider_type__in]
@@ -8167,6 +8207,7 @@ paths:
* `kubernetes` - Kubernetes
* `m365` - M365
* `github` - GitHub
* `iac` - IaC
* `oci` - Oracle Cloud Infrastructure
explode: false
style: form
@@ -14201,6 +14242,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
- type: object
title: Oracle Cloud Infrastructure (OCI) API Key Credentials
properties:
@@ -16127,6 +16179,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
- type: object
title: Oracle Cloud Infrastructure (OCI) API Key Credentials
properties:
@@ -16427,6 +16490,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
- type: object
title: Oracle Cloud Infrastructure (OCI) API Key Credentials
properties:
@@ -16745,6 +16819,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
- type: object
title: Oracle Cloud Infrastructure (OCI) API Key Credentials
properties:
+37
View File
@@ -1143,6 +1143,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",
},
]
),
)
@@ -1292,6 +1302,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",
),
]
),
)
+35 -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
from prowler.providers.oraclecloud.oci_provider import OciProvider
@@ -66,6 +67,7 @@ def return_prowler_provider(
| AzureProvider
| GcpProvider
| GithubProvider
| IacProvider
| KubernetesProvider
| M365Provider
| OciProvider
@@ -76,7 +78,7 @@ def return_prowler_provider(
provider (Provider): The provider object containing the provider type and associated secrets.
Returns:
AwsProvider | AzureProvider | GcpProvider | GithubProvider | KubernetesProvider | M365Provider | OciProvider: The corresponding provider class.
AwsProvider | AzureProvider | GcpProvider | GithubProvider | IacProvider | KubernetesProvider | M365Provider | OciProvider: The corresponding provider class.
Raises:
ValueError: If the provider type specified in `provider.provider` is not supported.
@@ -94,6 +96,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 Provider.ProviderChoices.OCI.value:
prowler_provider = OciProvider
case _:
@@ -132,6 +136,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", {})
@@ -149,6 +163,7 @@ def initialize_prowler_provider(
| AzureProvider
| GcpProvider
| GithubProvider
| IacProvider
| KubernetesProvider
| M365Provider
| OciProvider
@@ -160,8 +175,8 @@ def initialize_prowler_provider(
mutelist_processor (Processor): The mutelist processor object containing the mutelist configuration.
Returns:
AwsProvider | AzureProvider | GcpProvider | GithubProvider | KubernetesProvider | M365Provider | OciProvider: An instance of the corresponding provider class
(`AwsProvider`, `AzureProvider`, `GcpProvider`, `GithubProvider`, `KubernetesProvider`, `M365Provider` or `OciProvider`) initialized with the
AwsProvider | AzureProvider | GcpProvider | GithubProvider | IacProvider | KubernetesProvider | M365Provider | OciProvider: An instance of the corresponding provider class
(`AwsProvider`, `AzureProvider`, `GcpProvider`, `GithubProvider`, `IacProvider`, `KubernetesProvider`, `M365Provider` or `OciProvider`) initialized with the
provider's secrets.
"""
prowler_provider = return_prowler_provider(provider)
@@ -185,9 +200,23 @@ 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
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:
@@ -239,6 +239,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"],
},
{
"type": "object",
"title": "Oracle Cloud Infrastructure (OCI) API Key Credentials",
+10
View File
@@ -1362,6 +1362,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:
@@ -1481,6 +1483,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 OracleCloudProviderSecret(serializers.Serializer):
user = serializers.CharField()
fingerprint = serializers.CharField()
+4
View File
@@ -113,6 +113,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
],
"oci": [
(lambda name: name.startswith("cis_"), OCICIS),
],
+1 -1
View File
@@ -32,7 +32,7 @@ The supported providers right now are:
| [M365](/user-guide/providers/microsoft365/getting-started-m365) | Official | UI, API, CLI |
| [Github](/user-guide/providers/github/getting-started-github) | Official | UI, API, CLI |
| [Oracle Cloud](/user-guide/providers/oci/getting-started-oci) | Official | UI, API, CLI |
| [Infra as Code](/user-guide/providers/iac/getting-started-iac) | Official | CLI |
| [Infra as Code](/user-guide/providers/iac/getting-started-iac) | Official | UI, API, CLI |
| [MongoDB Atlas](/user-guide/providers/mongodbatlas/getting-started-mongodbatlas) | Official | CLI |
| [LLM](/user-guide/providers/llm/getting-started-llm) | Official | CLI |
| **NHN** | Unofficial | CLI |
+5
View File
@@ -47,6 +47,11 @@ All notable changes to the **Prowler SDK** are documented in this file.
---
### Changed
- Adapt IaC provider to be used in the Prowler App [(#8751)](https://github.com/prowler-cloud/prowler/pull/8751)
---
## [v5.13.0] (Prowler v5.13.0)
### Added
+6
View File
@@ -359,6 +359,12 @@ def prowler():
else:
# Original behavior for IAC or non-verbose LLM
findings = global_provider.run()
# Note: IaC doesn't support granular progress tracking since Trivy runs as a black box
# and returns all findings at once. Progress tracking would just be 0% → 100%.
# Filter findings by status if specified
if hasattr(args, "status") and args.status:
findings = [f for f in findings if f.status in args.status]
# Report findings for verbose output
report(findings, global_provider, output_options)
elif len(checks_to_execute):
+15 -4
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
elif provider.type == "llm":
@@ -407,6 +414,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
elif provider.type == "oci":
finding.compartment_id = getattr(finding, "compartment_id", "")
+105 -24
View File
@@ -1,4 +1,5 @@
import datetime
from datetime import timezone
from types import SimpleNamespace
from typing import Generator
@@ -25,6 +26,7 @@ from prowler.lib.scan.exceptions.exceptions import (
)
from prowler.providers.common.models import Audit_Metadata, ProviderOutputOptions
from prowler.providers.common.provider import Provider
from prowler.providers.iac.iac_provider import IacProvider
class Scan:
@@ -90,15 +92,25 @@ class Scan:
except ValueError:
raise ScanInvalidStatusError(f"Invalid status provided: {s}.")
# Load bulk compliance frameworks
self._bulk_compliance_frameworks = Compliance.get_bulk(provider.type)
# Get bulk checks metadata for the provider
self._bulk_checks_metadata = CheckMetadata.get_bulk(provider.type)
# Complete checks metadata with the compliance framework specification
self._bulk_checks_metadata = update_checks_metadata_with_compliance(
self._bulk_compliance_frameworks, self._bulk_checks_metadata
)
# 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 = {}
self._bulk_compliance_frameworks = {}
else:
# Load bulk compliance frameworks
self._bulk_compliance_frameworks = Compliance.get_bulk(provider.type)
# Get bulk checks metadata for the provider
self._bulk_checks_metadata = CheckMetadata.get_bulk(provider.type)
# Complete checks metadata with the compliance framework specification
self._bulk_checks_metadata = update_checks_metadata_with_compliance(
self._bulk_compliance_frameworks, self._bulk_checks_metadata
)
# Create a list of valid categories
valid_categories = set()
@@ -148,19 +160,22 @@ class Scan:
)
# Load checks to execute
self._checks_to_execute = sorted(
load_checks_to_execute(
bulk_checks_metadata=self._bulk_checks_metadata,
bulk_compliance_frameworks=self._bulk_compliance_frameworks,
check_list=checks,
service_list=services,
compliance_frameworks=compliances,
categories=categories,
severities=severities,
provider=provider.type,
checks_file=None,
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,
bulk_compliance_frameworks=self._bulk_compliance_frameworks,
check_list=checks,
service_list=services,
compliance_frameworks=compliances,
categories=categories,
severities=severities,
provider=provider.type,
checks_file=None,
)
)
)
# Exclude checks
if excluded_checks:
@@ -184,9 +199,13 @@ class Scan:
self._number_of_checks_to_execute = len(self._checks_to_execute)
service_checks_to_execute = get_service_checks_to_execute(
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
)
service_checks_completed = dict()
self._service_checks_to_execute = service_checks_to_execute
@@ -245,6 +264,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 +288,64 @@ 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
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:
# 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)
# Filter the findings by the status
if self._status:
findings = [f for f in findings if f.status in self._status]
# 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
@@ -349,6 +429,7 @@ 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:
check_name = check_name or "Scan error"
logger.error(
f"{check_name} - {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
)
+194 -39
View File
@@ -1,10 +1,11 @@
import json
import re
import shutil
import subprocess
import sys
import tempfile
from os import environ
from typing import List
from typing import Generator, List
from alive_progress import alive_bar
from colorama import Fore, Style
@@ -17,7 +18,7 @@ from prowler.config.config import (
from prowler.lib.check.models import CheckReportIAC
from prowler.lib.logger import logger
from prowler.lib.utils.utils import print_boxes
from prowler.providers.common.models import Audit_Metadata
from prowler.providers.common.models import Audit_Metadata, Connection
from prowler.providers.common.provider import Provider
@@ -173,7 +174,7 @@ class IacProvider(Provider):
"Severity": finding["Severity"],
"ResourceType": "iac",
"Description": finding_description,
"Risk": "",
"Risk": "This provider has not defined a risk for this check.",
"RelatedUrl": finding.get("PrimaryURL", ""),
"Remediation": {
"Code": {
@@ -242,20 +243,39 @@ class IacProvider(Provider):
logger.info(
f"Cloning repository {original_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 {original_url}..."
# 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",
spinner="classic",
stats=False,
enrich_print=False,
) as bar:
try:
bar.title = f"-> Cloning {original_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
else:
# No TTY, just clone without progress bar
logger.info(f"Cloning {original_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
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(
@@ -275,7 +295,10 @@ class IacProvider(Provider):
scan_dir = self.scan_path
try:
reports = self.run_scan(scan_dir, self.scanners, self.exclude_path)
# Collect all batches from the generator
reports = []
for batch in self.run_scan(scan_dir, self.scanners, self.exclude_path):
reports.extend(batch)
finally:
if temp_dir:
logger.info(f"Removing temporary directory {temp_dir}...")
@@ -285,7 +308,7 @@ class IacProvider(Provider):
def run_scan(
self, directory: str, scanners: list[str], exclude_path: list[str]
) -> List[CheckReportIAC]:
) -> Generator[List[CheckReportIAC], None, None]:
try:
logger.info(f"Running IaC scan on {directory} ...")
trivy_command = [
@@ -302,25 +325,47 @@ class IacProvider(Provider):
]
if exclude_path:
trivy_command.extend(["--skip-dirs", ",".join(exclude_path)])
with alive_bar(
ctrl_c=False,
bar="blocks",
spinner="classic",
stats=False,
enrich_print=False,
) as bar:
try:
bar.title = f"-> Running IaC scan on {directory} ..."
# Run Trivy with JSON output
# Check if we're in an environment with a TTY
try:
if sys.stdout.isatty():
with alive_bar(
ctrl_c=False,
bar="blocks",
spinner="classic",
stats=False,
enrich_print=False,
) as bar:
try:
bar.title = f"-> Running IaC scan on {directory} ..."
# Run Trivy with JSON output
process = subprocess.run(
trivy_command,
capture_output=True,
text=True,
)
bar.title = "-> Scan completed!"
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,
)
bar.title = "-> Scan completed!"
except Exception as error:
bar.title = "-> Scan failed!"
raise error
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"):
@@ -354,14 +399,15 @@ class IacProvider(Provider):
if not output:
logger.warning("No findings returned from Trivy scan")
return []
return
except Exception as error:
logger.critical(
f"{error.__class__.__name__}:{error.__traceback__.tb_lineno} -- {error}"
)
sys.exit(1)
reports = []
batch = []
batch_size = 100
# Process all trivy findings
for finding in output:
@@ -371,27 +417,44 @@ class IacProvider(Provider):
report = self._process_finding(
misconfiguration, finding["Target"], finding["Type"]
)
reports.append(report)
batch.append(report)
if len(batch) >= batch_size:
yield batch
batch = []
# Process Vulnerabilities
for vulnerability in finding.get("Vulnerabilities", []):
report = self._process_finding(
vulnerability, finding["Target"], finding["Type"]
)
reports.append(report)
batch.append(report)
if len(batch) >= batch_size:
yield batch
batch = []
# Process Secrets
for secret in finding.get("Secrets", []):
report = self._process_finding(
secret, finding["Target"], finding["Class"]
)
reports.append(report)
batch.append(report)
if len(batch) >= batch_size:
yield batch
batch = []
# Process Licenses
for license in finding.get("Licenses", []):
report = self._process_finding(
license, finding["Target"], finding["Type"]
)
reports.append(report)
batch.append(report)
if len(batch) >= batch_size:
yield batch
batch = []
return reports
# Yield any remaining findings in the last batch
if batch:
yield batch
except Exception as error:
if "No such file or directory: 'trivy'" in str(error):
@@ -434,3 +497,95 @@ 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)
"""
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():
try:
if oauth_app_token:
# If token is provided, use it for authentication
# Extract the domain and path from the URL
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)}",
)
+35 -11
View File
@@ -260,16 +260,24 @@ class TestScan:
assert scan.get_completed_services() == set()
assert scan.get_completed_checks() == set()
@patch("importlib.import_module")
@patch("prowler.lib.scan.scan.load_checks_to_execute")
@patch("prowler.lib.scan.scan.update_checks_metadata_with_compliance")
@patch("prowler.lib.scan.scan.Compliance.get_bulk")
@patch("prowler.lib.scan.scan.CheckMetadata.get_bulk")
@patch("prowler.lib.scan.scan.import_check")
def test_scan(
mock_import_module,
self,
mock_import_check,
mock_get_bulk,
mock_compliance_get_bulk,
mock_update_checks_metadata,
mock_load_checks,
mock_global_provider,
mock_execute,
mock_logger,
mock_generate_output,
mock_recover_checks_from_provider,
mock_load_check_metadata,
):
from prowler.lib.check.models import Severity
mock_check_class = MagicMock()
mock_check_instance = mock_check_class.return_value
mock_check_instance.Provider = "aws"
@@ -277,23 +285,39 @@ class TestScan:
mock_check_instance.CheckTitle = "Check if IAM Access Analyzer is enabled"
mock_check_instance.Categories = []
mock_import_module.return_value = MagicMock(
mock_import_check.return_value = MagicMock(
accessanalyzer_enabled=mock_check_class
)
checks_to_execute = {"accessanalyzer_enabled"}
custom_checks_metadata = {}
mock_global_provider.type = "aws"
# Mock CheckMetadata
mock_metadata = MagicMock()
mock_metadata.CheckID = "accessanalyzer_enabled"
mock_metadata.ResourceType = "AWS::IAM::AccessAnalyzer"
mock_metadata.Categories = []
mock_metadata.CheckAliases = []
mock_metadata.Severity = Severity.medium
mock_metadata.Compliance = []
bulk_checks_metadata = {"accessanalyzer_enabled": mock_metadata}
mock_get_bulk.return_value = bulk_checks_metadata
# Mock update_checks_metadata_with_compliance to return the same metadata
mock_update_checks_metadata.return_value = bulk_checks_metadata
# Mock Compliance frameworks
mock_compliance_get_bulk.return_value = {}
# Mock load_checks_to_execute to return the checks
mock_load_checks.return_value = ["accessanalyzer_enabled"]
scan = Scan(mock_global_provider, checks=checks_to_execute)
mock_load_check_metadata.assert_called_once()
mock_recover_checks_from_provider.assert_called_once_with("aws")
results = list(scan.scan(custom_checks_metadata))
assert mock_generate_output.call_count == 1 * len(mock_execute.side_effect())
assert mock_execute.call_count == 1
assert len(results) == 1
assert results[0][1] == mock_execute.side_effect()
assert results[0][0] == 100.0
assert scan.progress == 100.0
# Since the scan is mocked, the duration will always be 0 for now
+62 -25
View File
@@ -86,9 +86,12 @@ class TestIacProvider:
stdout=get_sample_trivy_json_output(), stderr=""
)
reports = provider.run_scan(
# Collect all batches from the generator
reports = []
for batch in provider.run_scan(
"/test/directory", ["vuln", "misconfig", "secret"], []
)
):
reports.extend(batch)
# Should have 3 misconfigurations from the sample output
assert len(reports) == 3
@@ -124,9 +127,11 @@ class TestIacProvider:
stdout=get_empty_trivy_output(), stderr=""
)
reports = provider.run_scan(
reports = []
for batch in provider.run_scan(
"/test/directory", ["vuln", "misconfig", "secret"], []
)
):
reports.extend(batch)
assert len(reports) == 0
def test_provider_run_local_scan(self):
@@ -200,7 +205,9 @@ class TestIacProvider:
)
with pytest.raises(SystemExit) as excinfo:
provider.run_scan("/test/directory", ["all"], [])
# Consume the generator
for _ in provider.run_scan("/test/directory", ["all"], []):
pass
assert excinfo.value.code == 1
@@ -212,7 +219,11 @@ class TestIacProvider:
mock_subprocess.return_value = MagicMock(stdout="null", stderr="")
with pytest.raises(SystemExit) as exc_info:
provider.run_scan("/test/directory", ["vuln", "misconfig", "secret"], [])
# Consume the generator
for _ in provider.run_scan(
"/test/directory", ["vuln", "misconfig", "secret"], []
):
pass
assert exc_info.value.code == 1
def test_iac_provider_process_finding_dockerfile(self):
@@ -264,9 +275,11 @@ class TestIacProvider:
stdout=json.dumps(sample_output), stderr=""
)
result = provider.run_scan(
result = []
for batch in provider.run_scan(
"/test/directory", ["vuln", "misconfig", "secret"], []
)
):
result.extend(batch)
# Verify results
assert len(result) == 2
@@ -299,9 +312,11 @@ class TestIacProvider:
stdout=json.dumps(sample_output), stderr=""
)
result = provider.run_scan(
result = []
for batch in provider.run_scan(
"/test/directory", ["vuln", "misconfig", "secret"], ["exclude/path"]
)
):
result.extend(batch)
# Verify results
assert len(result) == 1
@@ -318,9 +333,11 @@ class TestIacProvider:
stdout=json.dumps({"Results": []}), stderr=""
)
result = provider.run_scan(
result = []
for batch in provider.run_scan(
"/test/directory", ["vuln", "misconfig", "secret"], []
)
):
result.extend(batch)
# Verify results
assert len(result) == 0
@@ -356,9 +373,11 @@ class TestIacProvider:
stdout=json.dumps(sample_output), stderr=""
)
result = provider.run_scan(
result = []
for batch in provider.run_scan(
"/test/directory", ["vuln", "misconfig", "secret"], []
)
):
result.extend(batch)
# Verify results
assert len(result) == 2
@@ -377,7 +396,11 @@ class TestIacProvider:
mock_subprocess.side_effect = Exception("Test exception")
with pytest.raises(SystemExit) as exc_info:
provider.run_scan("/test/directory", ["vuln", "misconfig", "secret"], [])
# Consume the generator
for _ in provider.run_scan(
"/test/directory", ["vuln", "misconfig", "secret"], []
):
pass
assert exc_info.value.code == 1
@@ -405,7 +428,9 @@ class TestIacProvider:
# Test with specific scanners
scanners = ["vuln", "misconfig", "secret"]
result = provider.run_scan("/test/directory", scanners, [])
result = []
for batch in provider.run_scan("/test/directory", scanners, []):
result.extend(batch)
# Verify subprocess was called with correct scanners
mock_subprocess.assert_called_once_with(
@@ -453,9 +478,11 @@ class TestIacProvider:
# Test with exclude paths
exclude_paths = ["node_modules", ".git", "vendor"]
result = provider.run_scan(
result = []
for batch in provider.run_scan(
"/test/directory", ["vuln", "misconfig", "secret"], exclude_paths
)
):
result.extend(batch)
# Verify subprocess was called with correct exclude paths
expected_command = [
@@ -510,9 +537,12 @@ class TestIacProvider:
stdout=json.dumps(sample_output), stderr=""
)
result = provider.run_scan(
# Consume the generator to get all batches
result = []
for batch in provider.run_scan(
"/test/directory", ["vuln", "misconfig", "secret"], []
)
):
result.extend(batch)
# Verify results
assert len(result) == 5 # 5 misconfigurations
@@ -536,9 +566,12 @@ class TestIacProvider:
stdout=json.dumps({"Results": []}), stderr=""
)
result = provider.run_scan(
# Consume the generator to get all batches
result = []
for batch in provider.run_scan(
"/test/directory", ["vuln", "misconfig", "secret"], []
)
):
result.extend(batch)
# Verify results
assert len(result) == 0
@@ -592,9 +625,12 @@ class TestIacProvider:
stdout=json.dumps(sample_output), stderr=""
)
result = provider.run_scan(
# Consume the generator to get all batches
result = []
for batch in provider.run_scan(
"/test/directory", ["vuln", "misconfig", "secret"], []
)
):
result.extend(batch)
# Verify results
assert (
@@ -615,7 +651,8 @@ class TestIacProvider:
)
with patch.object(provider, "run_scan") as mock_run_scan:
mock_run_scan.return_value = []
# Mock should return a generator (empty in this case)
mock_run_scan.return_value = iter([])
provider.run()
mock_run_scan.assert_called_once_with(
+3 -1
View File
@@ -8,6 +8,7 @@ All notable changes to the **Prowler UI** are documented in this file.
- RSS feeds support [(#9109)](https://github.com/prowler-cloud/prowler/pull/9109)
- Customer Support menu item [(#9143)](https://github.com/prowler-cloud/prowler/pull/9143)
- IaC (Infrastructure as Code) provider support for scanning remote repositories [(#8751)](https://github.com/prowler-cloud/prowler/pull/8751)
### 🔄 Changed
@@ -20,7 +21,6 @@ All notable changes to the **Prowler UI** are documented in this file.
### 🔄 Changed
- Upgrade React to version 19.2.0 [(#9039)](https://github.com/prowler-cloud/prowler/pull/9039)
- Add support for Oracle Cloud Infrastructure (OCI) provider [(#8984)](https://github.com/prowler-cloud/prowler/pull/8984)
---
@@ -87,6 +87,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
+18
View File
@@ -139,6 +139,24 @@ export const addCredentialsProvider = async (formData: FormData) => {
) as string | undefined;
try {
// For IaC provider, fetch the provider data to get the repository URL from uid
if (providerType === "iac") {
const providerUrl = new URL(`${apiBaseUrl}/providers/${providerId}`);
const providerResponse = await fetch(providerUrl.toString(), {
headers: await getAuthHeaders({ contentType: false }),
});
if (providerResponse.ok) {
const providerData = await providerResponse.json();
const providerUid = providerData?.data?.attributes?.uid;
// Add the repository URL to formData using the provider's uid
if (providerUid) {
formData.append(ProviderCredentialFields.REPOSITORY_URL, providerUid);
}
}
}
const { secretType, secret } = buildSecretConfig(
formData,
providerType,
@@ -8,6 +8,7 @@ import {
AzureProviderBadge,
GCPProviderBadge,
GitHubProviderBadge,
IacProviderBadge,
KS8ProviderBadge,
M365ProviderBadge,
OracleCloudProviderBadge,
@@ -28,6 +29,7 @@ const PROVIDER_ICON: Record<ProviderType, ReactNode> = {
kubernetes: <KS8ProviderBadge width={18} height={18} />,
m365: <M365ProviderBadge width={18} height={18} />,
github: <GitHubProviderBadge width={18} height={18} />,
iac: <IacProviderBadge width={18} height={18} />,
oci: <OracleCloudProviderBadge width={18} height={18} />,
};
@@ -42,6 +42,11 @@ const GitHubProviderBadge = lazy(() =>
default: m.GitHubProviderBadge,
})),
);
const IacProviderBadge = lazy(() =>
import("@/components/icons/providers-badge").then((m) => ({
default: m.IacProviderBadge,
})),
);
const OracleCloudProviderBadge = lazy(() =>
import("@/components/icons/providers-badge").then((m) => ({
default: m.OracleCloudProviderBadge,
@@ -82,6 +87,10 @@ const PROVIDER_DATA: Record<
label: "GitHub",
icon: GitHubProviderBadge,
},
iac: {
label: "Infrastructure as Code",
icon: IacProviderBadge,
},
oci: {
label: "Oracle Cloud Infrastructure",
icon: OracleCloudProviderBadge,
@@ -5,6 +5,7 @@ import {
AzureProviderBadge,
GCPProviderBadge,
GitHubProviderBadge,
IacProviderBadge,
KS8ProviderBadge,
M365ProviderBadge,
OracleCloudProviderBadge,
@@ -64,6 +65,15 @@ export const CustomProviderInputGitHub = () => {
);
};
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>
);
};
export const CustomProviderInputOracleCloud = () => {
return (
<div className="flex items-center gap-x-2">
@@ -11,6 +11,7 @@ import {
CustomProviderInputAzure,
CustomProviderInputGCP,
CustomProviderInputGitHub,
CustomProviderInputIac,
CustomProviderInputKubernetes,
CustomProviderInputM365,
CustomProviderInputOracleCloud,
@@ -44,6 +45,10 @@ const providerDisplayData: Record<
label: "GitHub",
component: <CustomProviderInputGitHub />,
},
iac: {
label: "Infrastructure as Code",
component: <CustomProviderInputIac />,
},
oci: {
label: "Oracle Cloud Infrastructure",
component: <CustomProviderInputOracleCloud />,
@@ -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>
);
@@ -2,6 +2,7 @@ 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";
export * from "./oraclecloud-provider-badge";
@@ -8,6 +8,7 @@ import {
AzureProviderBadge,
GCPProviderBadge,
GitHubProviderBadge,
IacProviderBadge,
KS8ProviderBadge,
M365ProviderBadge,
OracleCloudProviderBadge,
@@ -38,6 +39,8 @@ export const ProvidersOverview = ({
return <KS8ProviderBadge width={30} height={30} />;
case "github":
return <GitHubProviderBadge width={30} height={30} />;
case "iac":
return <IacProviderBadge width={30} height={30} />;
case "oci":
return <OracleCloudProviderBadge width={30} height={30} />;
default:
@@ -52,6 +55,7 @@ export const ProvidersOverview = ({
gcp: "GCP",
kubernetes: "Kubernetes",
github: "GitHub",
iac: "IaC",
oci: "OCI",
};
@@ -17,6 +17,7 @@ const providerTypeLabels: Record<ProviderType, string> = {
m365: "Microsoft 365",
kubernetes: "Kubernetes",
github: "GitHub",
iac: "Infrastructure as Code",
oci: "Oracle Cloud Infrastructure",
};
@@ -12,6 +12,7 @@ import {
AzureProviderBadge,
GCPProviderBadge,
GitHubProviderBadge,
IacProviderBadge,
KS8ProviderBadge,
M365ProviderBadge,
OracleCloudProviderBadge,
@@ -79,6 +80,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>
<CustomRadio
description="Oracle Cloud Infrastructure"
value="oci"
@@ -16,6 +16,7 @@ import {
AzureCredentials,
GCPDefaultCredentials,
GCPServiceAccountKey,
IacCredentials,
KubernetesCredentials,
M365CertificateCredentials,
M365ClientSecretCredentials,
@@ -34,6 +35,7 @@ import {
} from "./select-credentials-type/m365";
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 { OracleCloudCredentialsForm } from "./via-credentials/oraclecloud-credentials-form";
@@ -160,6 +162,11 @@ export const BaseCredentialsForm = ({
credentialsType={searchParamsObj.get("via") || undefined}
/>
)}
{providerType === "iac" && (
<IacCredentialsForm
control={form.control as unknown as Control<IacCredentials>}
/>
)}
{providerType === "oci" && (
<OracleCloudCredentialsForm
control={form.control as unknown as Control<OCICredentials>}
@@ -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",
};
case "oci":
return {
label: "Tenancy OCID",
@@ -0,0 +1,33 @@
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 text-default-foreground leading-9 font-bold">
Connect via Repository
</div>
<div className="text-default-500 text-sm">
Provide an access token if the repository is private (optional).
</div>
</div>
<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}
/>
</>
);
};
@@ -1,3 +1,4 @@
export * from "./azure-credentials-form";
export * from "./github-credentials-form";
export * from "./iac-credentials-form";
export * from "./k8s-credentials-form";
@@ -5,6 +5,7 @@ import {
AzureProviderBadge,
GCPProviderBadge,
GitHubProviderBadge,
IacProviderBadge,
KS8ProviderBadge,
M365ProviderBadge,
OracleCloudProviderBadge,
@@ -25,6 +26,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} />;
case "oci":
return <OracleCloudProviderBadge width={35} height={35} />;
default:
@@ -46,6 +49,8 @@ export const getProviderName = (provider: ProviderType): string => {
return "Microsoft 365";
case "github":
return "GitHub";
case "iac":
return "Infrastructure as Code";
case "oci":
return "Oracle Cloud Infrastructure";
default:
+5
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",
};
case "oci":
return {
text: "Need help connecting your Oracle Cloud account?",
@@ -197,6 +197,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);
};
/**
* Utility function to safely encode a string to base64
* Handles UTF-8 characters properly without using deprecated APIs
@@ -286,6 +300,10 @@ export const buildSecretConfig = (
secretType: "static",
secret: buildGitHubSecret(formData),
}),
iac: () => ({
secretType: "static",
secret: buildIacSecret(formData),
}),
oci: () => ({
secretType: "static",
secret: buildOracleCloudSecret(formData, providerUid),
@@ -45,6 +45,10 @@ export const ProviderCredentialFields = {
GITHUB_APP_ID: "github_app_id",
GITHUB_APP_KEY: "github_app_key_content",
// IaC fields
REPOSITORY_URL: "repository_url",
ACCESS_TOKEN: "access_token",
// OCI fields
OCI_USER: "user",
OCI_FINGERPRINT: "fingerprint",
@@ -81,6 +85,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",
CERTIFICATE_CONTENT: "/data/attributes/secret/certificate_content",
OCI_USER: "/data/attributes/secret/user",
OCI_FINGERPRINT: "/data/attributes/secret/fingerprint",
+7
View File
@@ -248,6 +248,12 @@ export type KubernetesCredentials = {
[ProviderCredentialFields.PROVIDER_ID]: string;
};
export type IacCredentials = {
[ProviderCredentialFields.REPOSITORY_URL]: string;
[ProviderCredentialFields.ACCESS_TOKEN]?: string;
[ProviderCredentialFields.PROVIDER_ID]: string;
};
export type OCICredentials = {
[ProviderCredentialFields.OCI_USER]: string;
[ProviderCredentialFields.OCI_FINGERPRINT]: string;
@@ -264,6 +270,7 @@ export type CredentialsFormSchema =
| GCPDefaultCredentials
| GCPServiceAccountKey
| KubernetesCredentials
| IacCredentials
| M365Credentials
| OCICredentials;
+31 -17
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(),
}),
z.object({
providerType: z.literal("oci"),
[ProviderCredentialFields.PROVIDER_ALIAS]: z.string(),
@@ -196,28 +201,37 @@ export const addCredentialsFormSchema = (
.string()
.optional(),
}
: providerType === "oci"
: providerType === "iac"
? {
[ProviderCredentialFields.OCI_USER]: z
[ProviderCredentialFields.REPOSITORY_URL]: z
.string()
.min(1, "User OCID is required"),
[ProviderCredentialFields.OCI_FINGERPRINT]: z
.optional(),
[ProviderCredentialFields.ACCESS_TOKEN]: z
.string()
.min(1, "Fingerprint is required"),
[ProviderCredentialFields.OCI_KEY_CONTENT]: z
.string()
.min(1, "Private Key Content is required"),
[ProviderCredentialFields.OCI_TENANCY]: z
.string()
.min(1, "Tenancy OCID is required"),
[ProviderCredentialFields.OCI_REGION]: z
.string()
.min(1, "Region is required"),
[ProviderCredentialFields.OCI_PASS_PHRASE]: z
.union([z.string(), z.literal("")])
.optional(),
}
: {}),
: providerType === "oci"
? {
[ProviderCredentialFields.OCI_USER]: z
.string()
.min(1, "User OCID is required"),
[ProviderCredentialFields.OCI_FINGERPRINT]: z
.string()
.min(1, "Fingerprint is required"),
[ProviderCredentialFields.OCI_KEY_CONTENT]: z
.string()
.min(1, "Private Key Content is required"),
[ProviderCredentialFields.OCI_TENANCY]: z
.string()
.min(1, "Tenancy OCID is required"),
[ProviderCredentialFields.OCI_REGION]: z
.string()
.min(1, "Region is required"),
[ProviderCredentialFields.OCI_PASS_PHRASE]: z
.union([z.string(), z.literal("")])
.optional(),
}
: {}),
})
.superRefine((data: Record<string, any>, ctx) => {
if (providerType === "m365") {
+1
View File
@@ -5,6 +5,7 @@ export const PROVIDER_TYPES = [
"kubernetes",
"m365",
"github",
"iac",
"oci",
] as const;