mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-10-04 02:04:06 +00:00
feat(iac): add IaC to Prowler App (#8751)
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
]
|
||||
@@ -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(
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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",
|
||||
),
|
||||
]
|
||||
),
|
||||
)
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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),
|
||||
],
|
||||
|
||||
@@ -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 |
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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
@@ -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}"
|
||||
)
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
@@ -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") {
|
||||
|
||||
@@ -5,6 +5,7 @@ export const PROVIDER_TYPES = [
|
||||
"kubernetes",
|
||||
"m365",
|
||||
"github",
|
||||
"iac",
|
||||
"oci",
|
||||
] as const;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user