mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-08-23 06:12:00 +00:00
Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7732281ecf | ||
|
|
55e846b3d7 |
@@ -1,18 +1,4 @@
|
||||
name: 'SDK: CodeQL Config'
|
||||
paths:
|
||||
- 'prowler/'
|
||||
|
||||
name: "SDK - CodeQL Config"
|
||||
paths-ignore:
|
||||
- 'api/'
|
||||
- 'ui/'
|
||||
- 'dashboard/'
|
||||
- 'mcp_server/'
|
||||
- 'tests/**'
|
||||
- 'util/**'
|
||||
- 'contrib/**'
|
||||
- 'examples/**'
|
||||
- 'prowler/**/__pycache__/**'
|
||||
- 'prowler/**/*.md'
|
||||
|
||||
queries:
|
||||
- uses: security-and-quality
|
||||
- "api/"
|
||||
- "ui/"
|
||||
|
||||
@@ -1,17 +1,3 @@
|
||||
name: 'UI: CodeQL Config'
|
||||
name: "UI - CodeQL Config"
|
||||
paths:
|
||||
- 'ui/'
|
||||
|
||||
paths-ignore:
|
||||
- 'ui/node_modules/**'
|
||||
- 'ui/.next/**'
|
||||
- 'ui/out/**'
|
||||
- 'ui/tests/**'
|
||||
- 'ui/**/*.test.ts'
|
||||
- 'ui/**/*.test.tsx'
|
||||
- 'ui/**/*.spec.ts'
|
||||
- 'ui/**/*.spec.tsx'
|
||||
- 'ui/**/*.md'
|
||||
|
||||
queries:
|
||||
- uses: security-and-quality
|
||||
- "ui/"
|
||||
|
||||
@@ -1,34 +0,0 @@
|
||||
name: Label Community Contributors PRs
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened]
|
||||
|
||||
jobs:
|
||||
add-community-label:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
issues: write
|
||||
pull-requests: write
|
||||
|
||||
steps:
|
||||
- name: Label community contributors
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
# Fetch fresh PR data to get current author_association
|
||||
ASSOCIATION=$(gh api /repos/${{ github.repository }}/pulls/${{ github.event.number }} --jq '.author_association')
|
||||
AUTHOR=$(gh api /repos/${{ github.repository }}/pulls/${{ github.event.number }} --jq '.user.login')
|
||||
|
||||
echo "Author: $AUTHOR, Association: $ASSOCIATION"
|
||||
|
||||
# Members have associations like: OWNER, MEMBER, COLLABORATOR
|
||||
# Non-members have: CONTRIBUTOR, FIRST_TIME_CONTRIBUTOR, FIRST_TIMER, NONE
|
||||
if [[ "$ASSOCIATION" != "OWNER" && "$ASSOCIATION" != "MEMBER" && "$ASSOCIATION" != "COLLABORATOR" ]]; then
|
||||
gh api /repos/${{ github.repository }}/issues/${{ github.event.number }}/labels \
|
||||
-X POST \
|
||||
-f labels[]='community'
|
||||
echo "Added 'community' label for $ASSOCIATION contributor"
|
||||
else
|
||||
echo "Skipped labeling for $ASSOCIATION"
|
||||
fi
|
||||
@@ -1,80 +0,0 @@
|
||||
name: 'MCP: Pull Request'
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- 'master'
|
||||
- 'v5.*'
|
||||
paths:
|
||||
- '.github/workflows/mcp-pull-request.yml'
|
||||
- 'mcp_server/**'
|
||||
- '!mcp_server/README.md'
|
||||
- '!mcp_server/CHANGELOG.md'
|
||||
pull_request:
|
||||
branches:
|
||||
- 'master'
|
||||
- 'v5.*'
|
||||
paths:
|
||||
- '.github/workflows/mcp-pull-request.yml'
|
||||
- 'mcp_server/**'
|
||||
- '!mcp_server/README.md'
|
||||
- '!mcp_server/CHANGELOG.md'
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
env:
|
||||
MCP_WORKING_DIR: ./mcp_server
|
||||
IMAGE_NAME: prowler-mcp
|
||||
|
||||
jobs:
|
||||
dockerfile-lint:
|
||||
if: github.repository == 'prowler-cloud/prowler'
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 15
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
|
||||
- name: Lint Dockerfile with Hadolint
|
||||
uses: hadolint/hadolint-action@2332a7b74a6de0dda2e2221d575162eba76ba5e5 # v3.3.0
|
||||
with:
|
||||
dockerfile: mcp_server/Dockerfile
|
||||
|
||||
container-build-and-scan:
|
||||
if: github.repository == 'prowler-cloud/prowler'
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 30
|
||||
permissions:
|
||||
contents: read
|
||||
security-events: write
|
||||
pull-requests: write
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1
|
||||
|
||||
- name: Build MCP container
|
||||
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0
|
||||
with:
|
||||
context: ${{ env.MCP_WORKING_DIR }}
|
||||
push: false
|
||||
load: true
|
||||
tags: ${{ env.IMAGE_NAME }}:${{ github.sha }}
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
|
||||
- name: Scan MCP container with Trivy
|
||||
uses: ./.github/actions/trivy-scan
|
||||
with:
|
||||
image-name: ${{ env.IMAGE_NAME }}
|
||||
image-tag: ${{ github.sha }}
|
||||
fail-on-critical: 'false'
|
||||
severity: 'CRITICAL'
|
||||
@@ -150,8 +150,8 @@ jobs:
|
||||
# Remove --- separators
|
||||
sed -i '/^---$/d' "$output_file"
|
||||
|
||||
# Remove only trailing empty lines (not all empty lines)
|
||||
sed -i -e :a -e '/^\s*$/d;N;ba' "$output_file"
|
||||
# Remove trailing empty lines
|
||||
sed -i '/^$/d' "$output_file"
|
||||
}
|
||||
|
||||
# Calculate expected versions for this release
|
||||
@@ -247,11 +247,6 @@ jobs:
|
||||
echo "" >> combined_changelog.md
|
||||
fi
|
||||
|
||||
# Add fallback message if no changelogs were added
|
||||
if [ ! -s combined_changelog.md ]; then
|
||||
echo "No component changes detected for this release." >> combined_changelog.md
|
||||
fi
|
||||
|
||||
echo "Combined changelog preview:"
|
||||
cat combined_changelog.md
|
||||
|
||||
@@ -341,6 +336,13 @@ jobs:
|
||||
CURRENT_PROWLER_REF=$(grep 'prowler @ git+https://github.com/prowler-cloud/prowler.git@' api/pyproject.toml | sed -E 's/.*@([^"]+)".*/\1/' | tr -d '[:space:]')
|
||||
BRANCH_NAME_TRIMMED=$(echo "$BRANCH_NAME" | tr -d '[:space:]')
|
||||
|
||||
# Create a temporary branch for the PR from the minor version branch
|
||||
TEMP_BRANCH="update-api-dependency-$BRANCH_NAME_TRIMMED-$(date +%s)"
|
||||
echo "TEMP_BRANCH=$TEMP_BRANCH" >> $GITHUB_ENV
|
||||
|
||||
# Create temp branch from the current minor version branch
|
||||
git checkout -b "$TEMP_BRANCH"
|
||||
|
||||
# Minor release: update the dependency to use the release branch
|
||||
echo "Updating prowler dependency from '$CURRENT_PROWLER_REF' to '$BRANCH_NAME_TRIMMED'"
|
||||
sed -i "s|prowler @ git+https://github.com/prowler-cloud/prowler.git@[^\"]*\"|prowler @ git+https://github.com/prowler-cloud/prowler.git@$BRANCH_NAME_TRIMMED\"|" api/pyproject.toml
|
||||
@@ -358,6 +360,11 @@ jobs:
|
||||
poetry lock
|
||||
cd ..
|
||||
|
||||
# Commit and push the temporary branch
|
||||
git add api/pyproject.toml api/poetry.lock
|
||||
git commit -m "chore(api): update prowler dependency to $BRANCH_NAME_TRIMMED for release $PROWLER_VERSION"
|
||||
git push origin "$TEMP_BRANCH"
|
||||
|
||||
echo "✓ Prepared prowler dependency update to: $UPDATED_PROWLER_REF"
|
||||
|
||||
- name: Create PR for API dependency update
|
||||
@@ -365,12 +372,8 @@ jobs:
|
||||
uses: peter-evans/create-pull-request@271a8d0340265f705b14b6d32b9829c1cb33d45e # v7.0.8
|
||||
with:
|
||||
token: ${{ secrets.PROWLER_BOT_ACCESS_TOKEN }}
|
||||
commit-message: 'chore(api): update prowler dependency to ${{ env.BRANCH_NAME }} for release ${{ env.PROWLER_VERSION }}'
|
||||
branch: update-api-dependency-${{ env.BRANCH_NAME }}-${{ github.run_number }}
|
||||
branch: ${{ env.TEMP_BRANCH }}
|
||||
base: ${{ env.BRANCH_NAME }}
|
||||
add-paths: |
|
||||
api/pyproject.toml
|
||||
api/poetry.lock
|
||||
title: "chore(api): Update prowler dependency to ${{ env.BRANCH_NAME }} for release ${{ env.PROWLER_VERSION }}"
|
||||
body: |
|
||||
### Description
|
||||
@@ -403,6 +406,5 @@ jobs:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Clean up temporary files
|
||||
if: always()
|
||||
run: |
|
||||
rm -f prowler_changelog.md api_changelog.md ui_changelog.md mcp_changelog.md combined_changelog.md
|
||||
|
||||
@@ -1,40 +1,44 @@
|
||||
name: 'SDK: CodeQL'
|
||||
# For most projects, this workflow file will not need changing; you simply need
|
||||
# to commit it to your repository.
|
||||
#
|
||||
# You may wish to alter this file to override the set of languages analyzed,
|
||||
# or to provide custom queries or build logic.
|
||||
#
|
||||
# ******** NOTE ********
|
||||
# We have attempted to detect the languages in your repository. Please check
|
||||
# the `language` matrix defined below to confirm you have the correct set of
|
||||
# supported CodeQL languages.
|
||||
#
|
||||
name: SDK - CodeQL
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- 'master'
|
||||
- 'v5.*'
|
||||
paths:
|
||||
- 'prowler/**'
|
||||
- 'tests/**'
|
||||
- 'pyproject.toml'
|
||||
- '.github/workflows/sdk-codeql.yml'
|
||||
- '.github/codeql/sdk-codeql-config.yml'
|
||||
- '!prowler/CHANGELOG.md'
|
||||
- "master"
|
||||
- "v3"
|
||||
- "v4.*"
|
||||
- "v5.*"
|
||||
paths-ignore:
|
||||
- 'ui/**'
|
||||
- 'api/**'
|
||||
- '.github/**'
|
||||
pull_request:
|
||||
branches:
|
||||
- 'master'
|
||||
- 'v5.*'
|
||||
paths:
|
||||
- 'prowler/**'
|
||||
- 'tests/**'
|
||||
- 'pyproject.toml'
|
||||
- '.github/workflows/sdk-codeql.yml'
|
||||
- '.github/codeql/sdk-codeql-config.yml'
|
||||
- '!prowler/CHANGELOG.md'
|
||||
- "master"
|
||||
- "v3"
|
||||
- "v4.*"
|
||||
- "v5.*"
|
||||
paths-ignore:
|
||||
- 'ui/**'
|
||||
- 'api/**'
|
||||
- '.github/**'
|
||||
schedule:
|
||||
- cron: '00 12 * * *'
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
analyze:
|
||||
name: CodeQL Security Analysis
|
||||
name: Analyze
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 30
|
||||
permissions:
|
||||
actions: read
|
||||
contents: read
|
||||
@@ -43,20 +47,21 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
language:
|
||||
- 'python'
|
||||
language: [ 'python' ]
|
||||
# Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@3599b3baa15b485a2e49ef411a7a4bb2452e7f93 # v3.30.5
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
config-file: ./.github/codeql/sdk-codeql-config.yml
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@3599b3baa15b485a2e49ef411a7a4bb2452e7f93 # v3.30.5
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
config-file: ./.github/codeql/sdk-codeql-config.yml
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@3599b3baa15b485a2e49ef411a7a4bb2452e7f93 # v3.30.5
|
||||
with:
|
||||
category: '/language:${{ matrix.language }}'
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@3599b3baa15b485a2e49ef411a7a4bb2452e7f93 # v3.30.5
|
||||
with:
|
||||
category: "/language:${{matrix.language}}"
|
||||
|
||||
@@ -1,36 +1,36 @@
|
||||
name: 'UI: CodeQL'
|
||||
# For most projects, this workflow file will not need changing; you simply need
|
||||
# to commit it to your repository.
|
||||
#
|
||||
# You may wish to alter this file to override the set of languages analyzed,
|
||||
# or to provide custom queries or build logic.
|
||||
#
|
||||
# ******** NOTE ********
|
||||
# We have attempted to detect the languages in your repository. Please check
|
||||
# the `language` matrix defined below to confirm you have the correct set of
|
||||
# supported CodeQL languages.
|
||||
#
|
||||
name: UI - CodeQL
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- 'master'
|
||||
- 'v5.*'
|
||||
- "master"
|
||||
- "v5.*"
|
||||
paths:
|
||||
- 'ui/**'
|
||||
- '.github/workflows/ui-codeql.yml'
|
||||
- '.github/codeql/ui-codeql-config.yml'
|
||||
- '!ui/CHANGELOG.md'
|
||||
- "ui/**"
|
||||
pull_request:
|
||||
branches:
|
||||
- 'master'
|
||||
- 'v5.*'
|
||||
- "master"
|
||||
- "v5.*"
|
||||
paths:
|
||||
- 'ui/**'
|
||||
- '.github/workflows/ui-codeql.yml'
|
||||
- '.github/codeql/ui-codeql-config.yml'
|
||||
- '!ui/CHANGELOG.md'
|
||||
- "ui/**"
|
||||
schedule:
|
||||
- cron: '00 12 * * *'
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
- cron: "00 12 * * *"
|
||||
|
||||
jobs:
|
||||
analyze:
|
||||
name: CodeQL Security Analysis
|
||||
name: Analyze
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 30
|
||||
permissions:
|
||||
actions: read
|
||||
contents: read
|
||||
@@ -39,13 +39,14 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
language:
|
||||
- 'javascript-typescript'
|
||||
language: ["javascript"]
|
||||
# Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@3599b3baa15b485a2e49ef411a7a4bb2452e7f93 # v3.30.5
|
||||
with:
|
||||
@@ -55,4 +56,4 @@ jobs:
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@3599b3baa15b485a2e49ef411a7a4bb2452e7f93 # v3.30.5
|
||||
with:
|
||||
category: '/language:${{ matrix.language }}'
|
||||
category: "/language:${{matrix.language}}"
|
||||
|
||||
@@ -18,7 +18,6 @@ jobs:
|
||||
AUTH_TRUST_HOST: true
|
||||
NEXTAUTH_URL: 'http://localhost:3000'
|
||||
NEXT_PUBLIC_API_BASE_URL: 'http://localhost:8080/api/v1'
|
||||
E2E_NEW_PASSWORD: ${{ secrets.E2E_NEW_PASSWORD }}
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
|
||||
@@ -39,12 +39,6 @@ secrets-*/
|
||||
# JUnit Reports
|
||||
junit-reports/
|
||||
|
||||
# Test and coverage artifacts
|
||||
*_coverage.xml
|
||||
pytest_*.xml
|
||||
.coverage
|
||||
htmlcov/
|
||||
|
||||
# VSCode files
|
||||
.vscode/
|
||||
|
||||
|
||||
@@ -2,11 +2,6 @@
|
||||
|
||||
All notable changes to the **Prowler API** are documented in this file.
|
||||
|
||||
## [1.15.0] (Prowler UNRELEASED)
|
||||
|
||||
### Added
|
||||
- Support for configuring multiple LLM providers [(#8772)](https://github.com/prowler-cloud/prowler/pull/8772)
|
||||
|
||||
## [1.14.0] (Prowler 5.13.0)
|
||||
|
||||
### Added
|
||||
|
||||
+1
-1
@@ -43,7 +43,7 @@ name = "prowler-api"
|
||||
package-mode = false
|
||||
# Needed for the SDK compatibility
|
||||
requires-python = ">=3.11,<3.13"
|
||||
version = "1.15.0"
|
||||
version = "1.14.0"
|
||||
|
||||
[project.scripts]
|
||||
celery = "src.backend.config.settings.celery"
|
||||
|
||||
@@ -27,8 +27,6 @@ from api.models import (
|
||||
Finding,
|
||||
Integration,
|
||||
Invitation,
|
||||
LighthouseProviderConfiguration,
|
||||
LighthouseProviderModels,
|
||||
Membership,
|
||||
OverviewStatusChoices,
|
||||
PermissionChoices,
|
||||
@@ -930,45 +928,3 @@ class TenantApiKeyFilter(FilterSet):
|
||||
"revoked": ["exact"],
|
||||
"name": ["exact", "icontains"],
|
||||
}
|
||||
|
||||
|
||||
class LighthouseProviderConfigFilter(FilterSet):
|
||||
provider_type = ChoiceFilter(
|
||||
choices=LighthouseProviderConfiguration.LLMProviderChoices.choices
|
||||
)
|
||||
provider_type__in = ChoiceInFilter(
|
||||
choices=LighthouseProviderConfiguration.LLMProviderChoices.choices,
|
||||
field_name="provider_type",
|
||||
lookup_expr="in",
|
||||
)
|
||||
is_active = BooleanFilter()
|
||||
|
||||
class Meta:
|
||||
model = LighthouseProviderConfiguration
|
||||
fields = {
|
||||
"provider_type": ["exact", "in"],
|
||||
"is_active": ["exact"],
|
||||
}
|
||||
|
||||
|
||||
class LighthouseProviderModelsFilter(FilterSet):
|
||||
provider_type = ChoiceFilter(
|
||||
choices=LighthouseProviderConfiguration.LLMProviderChoices.choices,
|
||||
field_name="provider_configuration__provider_type",
|
||||
)
|
||||
provider_type__in = ChoiceInFilter(
|
||||
choices=LighthouseProviderConfiguration.LLMProviderChoices.choices,
|
||||
field_name="provider_configuration__provider_type",
|
||||
lookup_expr="in",
|
||||
)
|
||||
|
||||
# Allow filtering by model id
|
||||
model_id = CharFilter(field_name="model_id", lookup_expr="exact")
|
||||
model_id__icontains = CharFilter(field_name="model_id", lookup_expr="icontains")
|
||||
model_id__in = CharInFilter(field_name="model_id", lookup_expr="in")
|
||||
|
||||
class Meta:
|
||||
model = LighthouseProviderModels
|
||||
fields = {
|
||||
"model_id": ["exact", "icontains", "in"],
|
||||
}
|
||||
|
||||
@@ -1,266 +0,0 @@
|
||||
# Generated by Django 5.1.12 on 2025-10-09 07:50
|
||||
|
||||
import json
|
||||
import logging
|
||||
import uuid
|
||||
|
||||
import django.db.models.deletion
|
||||
from config.custom_logging import BackendLogger
|
||||
from cryptography.fernet import Fernet
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
import api.rls
|
||||
from api.db_router import MainRouter
|
||||
|
||||
logger = logging.getLogger(BackendLogger.API)
|
||||
|
||||
|
||||
def migrate_lighthouse_configs_forward(apps, schema_editor):
|
||||
"""
|
||||
Migrate data from old LighthouseConfiguration to new multi-provider models.
|
||||
Old system: one LighthouseConfiguration per tenant (always OpenAI).
|
||||
"""
|
||||
LighthouseConfiguration = apps.get_model("api", "LighthouseConfiguration")
|
||||
LighthouseProviderConfiguration = apps.get_model(
|
||||
"api", "LighthouseProviderConfiguration"
|
||||
)
|
||||
LighthouseTenantConfiguration = apps.get_model(
|
||||
"api", "LighthouseTenantConfiguration"
|
||||
)
|
||||
LighthouseProviderModels = apps.get_model("api", "LighthouseProviderModels")
|
||||
|
||||
fernet = Fernet(settings.SECRETS_ENCRYPTION_KEY.encode())
|
||||
|
||||
# Migrate only tenants that actually have a LighthouseConfiguration
|
||||
for old_config in (
|
||||
LighthouseConfiguration.objects.using(MainRouter.admin_db)
|
||||
.select_related("tenant")
|
||||
.all()
|
||||
):
|
||||
tenant = old_config.tenant
|
||||
tenant_id = str(tenant.id)
|
||||
|
||||
try:
|
||||
# Create OpenAI provider configuration for this tenant
|
||||
api_key_decrypted = fernet.decrypt(bytes(old_config.api_key)).decode()
|
||||
credentials_encrypted = fernet.encrypt(
|
||||
json.dumps({"api_key": api_key_decrypted}).encode()
|
||||
)
|
||||
provider_config = LighthouseProviderConfiguration.objects.using(
|
||||
MainRouter.admin_db
|
||||
).create(
|
||||
tenant=tenant,
|
||||
provider_type="openai",
|
||||
credentials=credentials_encrypted,
|
||||
is_active=old_config.is_active,
|
||||
)
|
||||
|
||||
# Create tenant configuration from old values
|
||||
LighthouseTenantConfiguration.objects.using(MainRouter.admin_db).create(
|
||||
tenant=tenant,
|
||||
business_context=old_config.business_context or "",
|
||||
default_provider="openai",
|
||||
default_models={"openai": old_config.model},
|
||||
)
|
||||
|
||||
# Create initial provider model record
|
||||
LighthouseProviderModels.objects.using(MainRouter.admin_db).create(
|
||||
tenant=tenant,
|
||||
provider_configuration=provider_config,
|
||||
model_id=old_config.model,
|
||||
model_name=old_config.model,
|
||||
default_parameters={},
|
||||
)
|
||||
|
||||
except Exception:
|
||||
logger.exception(
|
||||
"Failed to migrate lighthouse config for tenant %s", tenant_id
|
||||
)
|
||||
continue
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("api", "0049_compliancerequirementoverview_passed_failed_findings"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name="LighthouseProviderConfiguration",
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.UUIDField(
|
||||
default=uuid.uuid4,
|
||||
editable=False,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
),
|
||||
),
|
||||
("inserted_at", models.DateTimeField(auto_now_add=True)),
|
||||
("updated_at", models.DateTimeField(auto_now=True)),
|
||||
(
|
||||
"provider_type",
|
||||
models.CharField(
|
||||
choices=[("openai", "OpenAI")],
|
||||
help_text="LLM provider name",
|
||||
max_length=50,
|
||||
),
|
||||
),
|
||||
("base_url", models.URLField(blank=True, null=True)),
|
||||
(
|
||||
"credentials",
|
||||
models.BinaryField(
|
||||
help_text="Encrypted JSON credentials for the provider"
|
||||
),
|
||||
),
|
||||
("is_active", models.BooleanField(default=True)),
|
||||
],
|
||||
options={
|
||||
"db_table": "lighthouse_provider_configurations",
|
||||
"abstract": False,
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name="LighthouseProviderModels",
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.UUIDField(
|
||||
default=uuid.uuid4,
|
||||
editable=False,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
),
|
||||
),
|
||||
("inserted_at", models.DateTimeField(auto_now_add=True)),
|
||||
("updated_at", models.DateTimeField(auto_now=True)),
|
||||
("model_id", models.CharField(max_length=100)),
|
||||
("model_name", models.CharField(max_length=100)),
|
||||
("default_parameters", models.JSONField(blank=True, default=dict)),
|
||||
],
|
||||
options={
|
||||
"db_table": "lighthouse_provider_models",
|
||||
"abstract": False,
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name="LighthouseTenantConfiguration",
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.UUIDField(
|
||||
default=uuid.uuid4,
|
||||
editable=False,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
),
|
||||
),
|
||||
("inserted_at", models.DateTimeField(auto_now_add=True)),
|
||||
("updated_at", models.DateTimeField(auto_now=True)),
|
||||
("business_context", models.TextField(blank=True, default="")),
|
||||
("default_provider", models.CharField(blank=True, max_length=50)),
|
||||
("default_models", models.JSONField(blank=True, default=dict)),
|
||||
],
|
||||
options={
|
||||
"db_table": "lighthouse_tenant_config",
|
||||
"abstract": False,
|
||||
},
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="lighthouseproviderconfiguration",
|
||||
name="tenant",
|
||||
field=models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE, to="api.tenant"
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="lighthouseprovidermodels",
|
||||
name="provider_configuration",
|
||||
field=models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="available_models",
|
||||
to="api.lighthouseproviderconfiguration",
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="lighthouseprovidermodels",
|
||||
name="tenant",
|
||||
field=models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE, to="api.tenant"
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="lighthousetenantconfiguration",
|
||||
name="tenant",
|
||||
field=models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE, to="api.tenant"
|
||||
),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name="lighthouseproviderconfiguration",
|
||||
index=models.Index(
|
||||
fields=["tenant_id", "provider_type"], name="lh_pc_tenant_type_idx"
|
||||
),
|
||||
),
|
||||
migrations.AddConstraint(
|
||||
model_name="lighthouseproviderconfiguration",
|
||||
constraint=api.rls.RowLevelSecurityConstraint(
|
||||
"tenant_id",
|
||||
name="rls_on_lighthouseproviderconfiguration",
|
||||
statements=["SELECT", "INSERT", "UPDATE", "DELETE"],
|
||||
),
|
||||
),
|
||||
migrations.AddConstraint(
|
||||
model_name="lighthouseproviderconfiguration",
|
||||
constraint=models.UniqueConstraint(
|
||||
fields=("tenant_id", "provider_type"),
|
||||
name="unique_provider_config_per_tenant",
|
||||
),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name="lighthouseprovidermodels",
|
||||
index=models.Index(
|
||||
fields=["tenant_id", "provider_configuration"],
|
||||
name="lh_prov_models_cfg_idx",
|
||||
),
|
||||
),
|
||||
migrations.AddConstraint(
|
||||
model_name="lighthouseprovidermodels",
|
||||
constraint=api.rls.RowLevelSecurityConstraint(
|
||||
"tenant_id",
|
||||
name="rls_on_lighthouseprovidermodels",
|
||||
statements=["SELECT", "INSERT", "UPDATE", "DELETE"],
|
||||
),
|
||||
),
|
||||
migrations.AddConstraint(
|
||||
model_name="lighthouseprovidermodels",
|
||||
constraint=models.UniqueConstraint(
|
||||
fields=("tenant_id", "provider_configuration", "model_id"),
|
||||
name="unique_provider_model_per_configuration",
|
||||
),
|
||||
),
|
||||
migrations.AddConstraint(
|
||||
model_name="lighthousetenantconfiguration",
|
||||
constraint=api.rls.RowLevelSecurityConstraint(
|
||||
"tenant_id",
|
||||
name="rls_on_lighthousetenantconfiguration",
|
||||
statements=["SELECT", "INSERT", "UPDATE", "DELETE"],
|
||||
),
|
||||
),
|
||||
migrations.AddConstraint(
|
||||
model_name="lighthousetenantconfiguration",
|
||||
constraint=models.UniqueConstraint(
|
||||
fields=("tenant_id",), name="unique_tenant_lighthouse_config"
|
||||
),
|
||||
),
|
||||
# Migrate data from old LighthouseConfiguration to new tables
|
||||
# This runs after all tables, indexes, and constraints are created
|
||||
# The old Lighthouse configuration table is not removed, so reverse_code is noop
|
||||
# During rollbacks, the old Lighthouse configuration remains intact while the new tables are removed
|
||||
migrations.RunPython(
|
||||
migrate_lighthouse_configs_forward,
|
||||
reverse_code=migrations.RunPython.noop,
|
||||
),
|
||||
]
|
||||
+25
-181
@@ -1873,6 +1873,22 @@ class LighthouseConfiguration(RowLevelSecurityProtectedModel):
|
||||
def clean(self):
|
||||
super().clean()
|
||||
|
||||
# Validate temperature
|
||||
if not 0 <= self.temperature <= 1:
|
||||
raise ModelValidationError(
|
||||
detail="Temperature must be between 0 and 1",
|
||||
code="invalid_temperature",
|
||||
pointer="/data/attributes/temperature",
|
||||
)
|
||||
|
||||
# Validate max_tokens
|
||||
if not 500 <= self.max_tokens <= 5000:
|
||||
raise ModelValidationError(
|
||||
detail="Max tokens must be between 500 and 5000",
|
||||
code="invalid_max_tokens",
|
||||
pointer="/data/attributes/max_tokens",
|
||||
)
|
||||
|
||||
@property
|
||||
def api_key_decoded(self):
|
||||
"""Return the decrypted API key, or None if unavailable or invalid."""
|
||||
@@ -1897,6 +1913,15 @@ class LighthouseConfiguration(RowLevelSecurityProtectedModel):
|
||||
code="invalid_api_key",
|
||||
pointer="/data/attributes/api_key",
|
||||
)
|
||||
|
||||
# Validate OpenAI API key format
|
||||
openai_key_pattern = r"^sk-[\w-]+T3BlbkFJ[\w-]+$"
|
||||
if not re.match(openai_key_pattern, value):
|
||||
raise ModelValidationError(
|
||||
detail="Invalid OpenAI API key format.",
|
||||
code="invalid_api_key",
|
||||
pointer="/data/attributes/api_key",
|
||||
)
|
||||
self.api_key = fernet.encrypt(value.encode())
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
@@ -1959,184 +1984,3 @@ class Processor(RowLevelSecurityProtectedModel):
|
||||
|
||||
class JSONAPIMeta:
|
||||
resource_name = "processors"
|
||||
|
||||
|
||||
class LighthouseProviderConfiguration(RowLevelSecurityProtectedModel):
|
||||
"""
|
||||
Per-tenant configuration for an LLM provider (credentials, base URL, activation).
|
||||
|
||||
One configuration per provider type per tenant.
|
||||
"""
|
||||
|
||||
class LLMProviderChoices(models.TextChoices):
|
||||
OPENAI = "openai", _("OpenAI")
|
||||
|
||||
id = models.UUIDField(primary_key=True, default=uuid4, editable=False)
|
||||
inserted_at = models.DateTimeField(auto_now_add=True, editable=False)
|
||||
updated_at = models.DateTimeField(auto_now=True, editable=False)
|
||||
|
||||
provider_type = models.CharField(
|
||||
max_length=50,
|
||||
choices=LLMProviderChoices.choices,
|
||||
help_text="LLM provider name",
|
||||
)
|
||||
|
||||
# For OpenAI-compatible providers
|
||||
base_url = models.URLField(blank=True, null=True)
|
||||
|
||||
# Encrypted JSON for provider-specific auth
|
||||
credentials = models.BinaryField(
|
||||
blank=False, null=False, help_text="Encrypted JSON credentials for the provider"
|
||||
)
|
||||
|
||||
is_active = models.BooleanField(default=True)
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.get_provider_type_display()} ({self.tenant_id})"
|
||||
|
||||
def clean(self):
|
||||
super().clean()
|
||||
|
||||
@property
|
||||
def credentials_decoded(self):
|
||||
if not self.credentials:
|
||||
return None
|
||||
try:
|
||||
decrypted_data = fernet.decrypt(bytes(self.credentials))
|
||||
return json.loads(decrypted_data.decode())
|
||||
except (InvalidToken, json.JSONDecodeError) as e:
|
||||
logger.warning("Failed to decrypt provider credentials: %s", e)
|
||||
return None
|
||||
except Exception as e:
|
||||
logger.exception(
|
||||
"Unexpected error while decrypting provider credentials: %s", e
|
||||
)
|
||||
return None
|
||||
|
||||
@credentials_decoded.setter
|
||||
def credentials_decoded(self, value):
|
||||
"""
|
||||
Set and encrypt credentials (assumes serializer performed validation).
|
||||
"""
|
||||
if not value:
|
||||
raise ModelValidationError(
|
||||
detail="Credentials are required",
|
||||
code="invalid_credentials",
|
||||
pointer="/data/attributes/credentials",
|
||||
)
|
||||
self.credentials = fernet.encrypt(json.dumps(value).encode())
|
||||
|
||||
class Meta(RowLevelSecurityProtectedModel.Meta):
|
||||
db_table = "lighthouse_provider_configurations"
|
||||
|
||||
constraints = [
|
||||
RowLevelSecurityConstraint(
|
||||
field="tenant_id",
|
||||
name="rls_on_%(class)s",
|
||||
statements=["SELECT", "INSERT", "UPDATE", "DELETE"],
|
||||
),
|
||||
models.UniqueConstraint(
|
||||
fields=["tenant_id", "provider_type"],
|
||||
name="unique_provider_config_per_tenant",
|
||||
),
|
||||
]
|
||||
|
||||
indexes = [
|
||||
models.Index(
|
||||
fields=["tenant_id", "provider_type"],
|
||||
name="lh_pc_tenant_type_idx",
|
||||
),
|
||||
]
|
||||
|
||||
class JSONAPIMeta:
|
||||
resource_name = "lighthouse-providers"
|
||||
|
||||
|
||||
class LighthouseTenantConfiguration(RowLevelSecurityProtectedModel):
|
||||
"""
|
||||
Tenant-level Lighthouse settings (business context and defaults).
|
||||
One record per tenant.
|
||||
"""
|
||||
|
||||
id = models.UUIDField(primary_key=True, default=uuid4, editable=False)
|
||||
inserted_at = models.DateTimeField(auto_now_add=True, editable=False)
|
||||
updated_at = models.DateTimeField(auto_now=True, editable=False)
|
||||
|
||||
business_context = models.TextField(blank=True, default="")
|
||||
|
||||
# Preferred provider key (e.g., "openai", "bedrock", "openai_compatible")
|
||||
default_provider = models.CharField(max_length=50, blank=True)
|
||||
|
||||
# Mapping of provider -> model id, e.g., {"openai": "gpt-4o", "bedrock": "anthropic.claude-v2"}
|
||||
default_models = models.JSONField(default=dict, blank=True)
|
||||
|
||||
def __str__(self):
|
||||
return f"Lighthouse Tenant Config for {self.tenant_id}"
|
||||
|
||||
def clean(self):
|
||||
super().clean()
|
||||
|
||||
class Meta(RowLevelSecurityProtectedModel.Meta):
|
||||
db_table = "lighthouse_tenant_config"
|
||||
|
||||
constraints = [
|
||||
RowLevelSecurityConstraint(
|
||||
field="tenant_id",
|
||||
name="rls_on_%(class)s",
|
||||
statements=["SELECT", "INSERT", "UPDATE", "DELETE"],
|
||||
),
|
||||
models.UniqueConstraint(
|
||||
fields=["tenant_id"], name="unique_tenant_lighthouse_config"
|
||||
),
|
||||
]
|
||||
|
||||
class JSONAPIMeta:
|
||||
resource_name = "lighthouse-config"
|
||||
|
||||
|
||||
class LighthouseProviderModels(RowLevelSecurityProtectedModel):
|
||||
"""
|
||||
Per-tenant, per-provider configuration list of available LLM models.
|
||||
RLS-protected; populated via provider API using tenant-scoped credentials.
|
||||
"""
|
||||
|
||||
id = models.UUIDField(primary_key=True, default=uuid4, editable=False)
|
||||
inserted_at = models.DateTimeField(auto_now_add=True, editable=False)
|
||||
updated_at = models.DateTimeField(auto_now=True, editable=False)
|
||||
|
||||
# Scope to a specific provider configuration within a tenant
|
||||
provider_configuration = models.ForeignKey(
|
||||
LighthouseProviderConfiguration,
|
||||
on_delete=models.CASCADE,
|
||||
related_name="available_models",
|
||||
)
|
||||
model_id = models.CharField(max_length=100)
|
||||
|
||||
# Human-friendly model name
|
||||
model_name = models.CharField(max_length=100)
|
||||
|
||||
# Model-specific default parameters (e.g., temperature, max_tokens)
|
||||
default_parameters = models.JSONField(default=dict, blank=True)
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.provider_configuration.provider_type}:{self.model_id} ({self.tenant_id})"
|
||||
|
||||
class Meta(RowLevelSecurityProtectedModel.Meta):
|
||||
db_table = "lighthouse_provider_models"
|
||||
constraints = [
|
||||
RowLevelSecurityConstraint(
|
||||
field="tenant_id",
|
||||
name="rls_on_%(class)s",
|
||||
statements=["SELECT", "INSERT", "UPDATE", "DELETE"],
|
||||
),
|
||||
models.UniqueConstraint(
|
||||
fields=["tenant_id", "provider_configuration", "model_id"],
|
||||
name="unique_provider_model_per_configuration",
|
||||
),
|
||||
]
|
||||
indexes = [
|
||||
models.Index(
|
||||
fields=["tenant_id", "provider_configuration"],
|
||||
name="lh_prov_models_cfg_idx",
|
||||
),
|
||||
]
|
||||
|
||||
@@ -6,14 +6,7 @@ from django.dispatch import receiver
|
||||
from django_celery_results.backends.database import DatabaseBackend
|
||||
|
||||
from api.db_utils import delete_related_daily_task
|
||||
from api.models import (
|
||||
LighthouseProviderConfiguration,
|
||||
LighthouseTenantConfiguration,
|
||||
Membership,
|
||||
Provider,
|
||||
TenantAPIKey,
|
||||
User,
|
||||
)
|
||||
from api.models import Membership, Provider, TenantAPIKey, User
|
||||
|
||||
|
||||
def create_task_result_on_publish(sender=None, headers=None, **kwargs): # noqa: F841
|
||||
@@ -63,33 +56,3 @@ def revoke_membership_api_keys(sender, instance, **kwargs): # noqa: F841
|
||||
TenantAPIKey.objects.filter(
|
||||
entity=instance.user, tenant_id=instance.tenant.id
|
||||
).update(revoked=True)
|
||||
|
||||
|
||||
@receiver(pre_delete, sender=LighthouseProviderConfiguration)
|
||||
def cleanup_lighthouse_defaults_before_delete(sender, instance, **kwargs): # noqa: F841
|
||||
"""
|
||||
Ensure tenant Lighthouse defaults do not reference a soon-to-be-deleted provider.
|
||||
|
||||
This runs for both per-instance deletes and queryset (bulk) deletes.
|
||||
"""
|
||||
try:
|
||||
tenant_cfg = LighthouseTenantConfiguration.objects.get(
|
||||
tenant_id=instance.tenant_id
|
||||
)
|
||||
except LighthouseTenantConfiguration.DoesNotExist:
|
||||
return
|
||||
|
||||
updated = False
|
||||
defaults = tenant_cfg.default_models or {}
|
||||
|
||||
if instance.provider_type in defaults:
|
||||
defaults.pop(instance.provider_type, None)
|
||||
tenant_cfg.default_models = defaults
|
||||
updated = True
|
||||
|
||||
if tenant_cfg.default_provider == instance.provider_type:
|
||||
tenant_cfg.default_provider = ""
|
||||
updated = True
|
||||
|
||||
if updated:
|
||||
tenant_cfg.save()
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -35,9 +35,6 @@ from api.db_router import MainRouter
|
||||
from api.models import (
|
||||
Integration,
|
||||
Invitation,
|
||||
LighthouseProviderConfiguration,
|
||||
LighthouseProviderModels,
|
||||
LighthouseTenantConfiguration,
|
||||
Membership,
|
||||
Processor,
|
||||
Provider,
|
||||
@@ -8706,483 +8703,3 @@ class TestTenantApiKeyViewSet:
|
||||
# Verify error object structure
|
||||
error = response_data["errors"][0]
|
||||
assert "detail" in error or "title" in error
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
class TestLighthouseTenantConfigViewSet:
|
||||
"""Test Lighthouse tenant configuration endpoint (singleton pattern)"""
|
||||
|
||||
def test_lighthouse_tenant_config_create_via_patch(self, authenticated_client):
|
||||
"""Test creating a tenant config successfully via PATCH (upsert)"""
|
||||
payload = {
|
||||
"data": {
|
||||
"type": "lighthouse-config",
|
||||
"attributes": {
|
||||
"business_context": "Test business context for security analysis",
|
||||
"default_provider": "",
|
||||
"default_models": {},
|
||||
},
|
||||
}
|
||||
}
|
||||
response = authenticated_client.patch(
|
||||
reverse("lighthouse-config"),
|
||||
data=payload,
|
||||
content_type=API_JSON_CONTENT_TYPE,
|
||||
)
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
data = response.json()["data"]
|
||||
assert (
|
||||
data["attributes"]["business_context"]
|
||||
== "Test business context for security analysis"
|
||||
)
|
||||
assert data["attributes"]["default_provider"] == ""
|
||||
assert data["attributes"]["default_models"] == {}
|
||||
|
||||
def test_lighthouse_tenant_config_upsert_behavior(self, authenticated_client):
|
||||
"""Test that PATCH creates config if not exists and updates if exists (upsert)"""
|
||||
payload = {
|
||||
"data": {
|
||||
"type": "lighthouse-config",
|
||||
"attributes": {
|
||||
"business_context": "First config",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
# First PATCH creates the config
|
||||
response = authenticated_client.patch(
|
||||
reverse("lighthouse-config"),
|
||||
data=payload,
|
||||
content_type=API_JSON_CONTENT_TYPE,
|
||||
)
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
first_data = response.json()["data"]
|
||||
assert first_data["attributes"]["business_context"] == "First config"
|
||||
|
||||
# Second PATCH updates the same config (not creating a duplicate)
|
||||
payload["data"]["attributes"]["business_context"] = "Updated config"
|
||||
response = authenticated_client.patch(
|
||||
reverse("lighthouse-config"),
|
||||
data=payload,
|
||||
content_type=API_JSON_CONTENT_TYPE,
|
||||
)
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
second_data = response.json()["data"]
|
||||
assert second_data["attributes"]["business_context"] == "Updated config"
|
||||
# Verify it's the same config (same ID)
|
||||
assert first_data["id"] == second_data["id"]
|
||||
|
||||
@patch("openai.OpenAI")
|
||||
def test_lighthouse_tenant_config_retrieve(
|
||||
self, mock_openai_client, authenticated_client, tenants_fixture
|
||||
):
|
||||
"""Test retrieving the singleton tenant config with proper provider and model validation"""
|
||||
|
||||
# Mock OpenAI client and models response
|
||||
mock_models_response = Mock()
|
||||
mock_models_response.data = [
|
||||
Mock(id="gpt-4o"),
|
||||
Mock(id="gpt-4o-mini"),
|
||||
Mock(id="gpt-5"),
|
||||
]
|
||||
mock_openai_client.return_value.models.list.return_value = mock_models_response
|
||||
|
||||
# Create OpenAI provider configuration
|
||||
provider_config = LighthouseProviderConfiguration.objects.create(
|
||||
tenant_id=tenants_fixture[0].id,
|
||||
provider_type="openai",
|
||||
credentials=b'{"api_key": "sk-test1234567890T3BlbkFJtest1234567890"}',
|
||||
is_active=True,
|
||||
)
|
||||
|
||||
# Create provider models (simulating refresh)
|
||||
LighthouseProviderModels.objects.create(
|
||||
tenant_id=tenants_fixture[0].id,
|
||||
provider_configuration=provider_config,
|
||||
model_id="gpt-4o",
|
||||
default_parameters={},
|
||||
)
|
||||
LighthouseProviderModels.objects.create(
|
||||
tenant_id=tenants_fixture[0].id,
|
||||
provider_configuration=provider_config,
|
||||
model_id="gpt-4o-mini",
|
||||
default_parameters={},
|
||||
)
|
||||
|
||||
# Create tenant configuration with valid provider and model
|
||||
config = LighthouseTenantConfiguration.objects.create(
|
||||
tenant_id=tenants_fixture[0].id,
|
||||
business_context="Test context",
|
||||
default_provider="openai",
|
||||
default_models={"openai": "gpt-4o"},
|
||||
)
|
||||
|
||||
# Retrieve and verify the configuration
|
||||
response = authenticated_client.get(reverse("lighthouse-config"))
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
data = response.json()["data"]
|
||||
assert data["id"] == str(config.id)
|
||||
assert data["attributes"]["business_context"] == "Test context"
|
||||
assert data["attributes"]["default_provider"] == "openai"
|
||||
assert data["attributes"]["default_models"] == {"openai": "gpt-4o"}
|
||||
|
||||
def test_lighthouse_tenant_config_retrieve_not_found(self, authenticated_client):
|
||||
"""Test GET when config doesn't exist returns 404"""
|
||||
response = authenticated_client.get(reverse("lighthouse-config"))
|
||||
assert response.status_code == status.HTTP_404_NOT_FOUND
|
||||
assert "not found" in response.json()["errors"][0]["detail"].lower()
|
||||
|
||||
def test_lighthouse_tenant_config_partial_update(
|
||||
self, authenticated_client, tenants_fixture
|
||||
):
|
||||
"""Test updating tenant config fields"""
|
||||
from api.models import LighthouseTenantConfiguration
|
||||
|
||||
# Create config first
|
||||
config = LighthouseTenantConfiguration.objects.create(
|
||||
tenant_id=tenants_fixture[0].id,
|
||||
business_context="Original context",
|
||||
default_provider="",
|
||||
default_models={},
|
||||
)
|
||||
|
||||
# Update it
|
||||
payload = {
|
||||
"data": {
|
||||
"type": "lighthouse-config",
|
||||
"attributes": {
|
||||
"business_context": "Updated context for cloud security",
|
||||
},
|
||||
}
|
||||
}
|
||||
response = authenticated_client.patch(
|
||||
reverse("lighthouse-config"),
|
||||
data=payload,
|
||||
content_type=API_JSON_CONTENT_TYPE,
|
||||
)
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
|
||||
# Verify update
|
||||
config.refresh_from_db()
|
||||
assert config.business_context == "Updated context for cloud security"
|
||||
|
||||
def test_lighthouse_tenant_config_update_invalid_provider(
|
||||
self, authenticated_client, tenants_fixture
|
||||
):
|
||||
"""Test validation fails when default_provider is not configured and active"""
|
||||
from api.models import LighthouseTenantConfiguration
|
||||
|
||||
# Create config first
|
||||
LighthouseTenantConfiguration.objects.create(
|
||||
tenant_id=tenants_fixture[0].id,
|
||||
business_context="Test",
|
||||
)
|
||||
|
||||
# Try to set invalid provider
|
||||
payload = {
|
||||
"data": {
|
||||
"type": "lighthouse-config",
|
||||
"attributes": {
|
||||
"default_provider": "nonexistent-provider",
|
||||
},
|
||||
}
|
||||
}
|
||||
response = authenticated_client.patch(
|
||||
reverse("lighthouse-config"),
|
||||
data=payload,
|
||||
content_type=API_JSON_CONTENT_TYPE,
|
||||
)
|
||||
assert response.status_code == status.HTTP_400_BAD_REQUEST
|
||||
assert "provider" in response.json()["errors"][0]["detail"].lower()
|
||||
|
||||
def test_lighthouse_tenant_config_update_invalid_json_format(
|
||||
self, authenticated_client, tenants_fixture
|
||||
):
|
||||
"""Test that invalid JSON payload is rejected"""
|
||||
from api.models import LighthouseTenantConfiguration
|
||||
|
||||
# Create config first
|
||||
LighthouseTenantConfiguration.objects.create(
|
||||
tenant_id=tenants_fixture[0].id,
|
||||
business_context="Test",
|
||||
)
|
||||
|
||||
# Send invalid JSON
|
||||
response = authenticated_client.patch(
|
||||
reverse("lighthouse-config"),
|
||||
data="invalid json",
|
||||
content_type=API_JSON_CONTENT_TYPE,
|
||||
)
|
||||
assert response.status_code == status.HTTP_400_BAD_REQUEST
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
class TestLighthouseProviderConfigViewSet:
|
||||
"""Tests for LighthouseProviderConfiguration create validations"""
|
||||
|
||||
def test_invalid_provider_type(self, authenticated_client):
|
||||
"""Add invalid provider (testprovider) should error"""
|
||||
payload = {
|
||||
"data": {
|
||||
"type": "lighthouse-providers",
|
||||
"attributes": {
|
||||
"provider_type": "testprovider",
|
||||
"credentials": {"api_key": "sk-testT3BlbkFJkey"},
|
||||
},
|
||||
}
|
||||
}
|
||||
resp = authenticated_client.post(
|
||||
reverse("lighthouse-providers-list"),
|
||||
data=payload,
|
||||
content_type=API_JSON_CONTENT_TYPE,
|
||||
)
|
||||
assert resp.status_code == status.HTTP_400_BAD_REQUEST
|
||||
|
||||
def test_openai_missing_credentials(self, authenticated_client):
|
||||
"""OpenAI provider without credentials should error"""
|
||||
payload = {
|
||||
"data": {
|
||||
"type": "lighthouse-providers",
|
||||
"attributes": {
|
||||
"provider_type": "openai",
|
||||
},
|
||||
}
|
||||
}
|
||||
resp = authenticated_client.post(
|
||||
reverse("lighthouse-providers-list"),
|
||||
data=payload,
|
||||
content_type=API_JSON_CONTENT_TYPE,
|
||||
)
|
||||
assert resp.status_code == status.HTTP_400_BAD_REQUEST
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"credentials",
|
||||
[
|
||||
{}, # empty credentials
|
||||
{"token": "sk-testT3BlbkFJkey"}, # wrong key name
|
||||
{"api_key": "ks-invalid-format"}, # wrong format
|
||||
],
|
||||
)
|
||||
def test_openai_invalid_credentials(self, authenticated_client, credentials):
|
||||
"""OpenAI provider with invalid credentials should error"""
|
||||
payload = {
|
||||
"data": {
|
||||
"type": "lighthouse-providers",
|
||||
"attributes": {
|
||||
"provider_type": "openai",
|
||||
"credentials": credentials,
|
||||
},
|
||||
}
|
||||
}
|
||||
resp = authenticated_client.post(
|
||||
reverse("lighthouse-providers-list"),
|
||||
data=payload,
|
||||
content_type=API_JSON_CONTENT_TYPE,
|
||||
)
|
||||
assert resp.status_code == status.HTTP_400_BAD_REQUEST
|
||||
|
||||
def test_openai_valid_credentials_success(self, authenticated_client):
|
||||
"""OpenAI provider with valid sk-xxx format should succeed"""
|
||||
valid_key = "sk-abc123T3BlbkFJxyz456"
|
||||
payload = {
|
||||
"data": {
|
||||
"type": "lighthouse-providers",
|
||||
"attributes": {
|
||||
"provider_type": "openai",
|
||||
"credentials": {"api_key": valid_key},
|
||||
},
|
||||
}
|
||||
}
|
||||
resp = authenticated_client.post(
|
||||
reverse("lighthouse-providers-list"),
|
||||
data=payload,
|
||||
content_type=API_JSON_CONTENT_TYPE,
|
||||
)
|
||||
assert resp.status_code == status.HTTP_201_CREATED
|
||||
data = resp.json()["data"]
|
||||
|
||||
masked_creds = data["attributes"].get("credentials")
|
||||
assert masked_creds is not None
|
||||
assert "api_key" in masked_creds
|
||||
assert masked_creds["api_key"] == ("*" * len(valid_key))
|
||||
|
||||
def test_openai_provider_duplicate_per_tenant(self, authenticated_client):
|
||||
"""If an OpenAI provider exists for tenant, creating again should error"""
|
||||
valid_key = "sk-dup123T3BlbkFJdup456"
|
||||
payload = {
|
||||
"data": {
|
||||
"type": "lighthouse-providers",
|
||||
"attributes": {
|
||||
"provider_type": "openai",
|
||||
"credentials": {"api_key": valid_key},
|
||||
},
|
||||
}
|
||||
}
|
||||
# First creation succeeds
|
||||
resp1 = authenticated_client.post(
|
||||
reverse("lighthouse-providers-list"),
|
||||
data=payload,
|
||||
content_type=API_JSON_CONTENT_TYPE,
|
||||
)
|
||||
assert resp1.status_code == status.HTTP_201_CREATED
|
||||
|
||||
# Second creation should fail with validation error
|
||||
resp2 = authenticated_client.post(
|
||||
reverse("lighthouse-providers-list"),
|
||||
data=payload,
|
||||
content_type=API_JSON_CONTENT_TYPE,
|
||||
)
|
||||
assert resp2.status_code == status.HTTP_400_BAD_REQUEST
|
||||
assert "already exists" in str(resp2.json()).lower()
|
||||
|
||||
def test_openai_patch_base_url_and_is_active(self, authenticated_client):
|
||||
"""After creating, should be able to patch base_url and is_active"""
|
||||
valid_key = "sk-patch123T3BlbkFJpatch456"
|
||||
create_payload = {
|
||||
"data": {
|
||||
"type": "lighthouse-providers",
|
||||
"attributes": {
|
||||
"provider_type": "openai",
|
||||
"credentials": {"api_key": valid_key},
|
||||
},
|
||||
}
|
||||
}
|
||||
create_resp = authenticated_client.post(
|
||||
reverse("lighthouse-providers-list"),
|
||||
data=create_payload,
|
||||
content_type=API_JSON_CONTENT_TYPE,
|
||||
)
|
||||
assert create_resp.status_code == status.HTTP_201_CREATED
|
||||
provider_id = create_resp.json()["data"]["id"]
|
||||
|
||||
patch_payload = {
|
||||
"data": {
|
||||
"type": "lighthouse-providers",
|
||||
"id": provider_id,
|
||||
"attributes": {
|
||||
"base_url": "https://api.example.com/v1",
|
||||
"is_active": False,
|
||||
},
|
||||
}
|
||||
}
|
||||
patch_resp = authenticated_client.patch(
|
||||
reverse("lighthouse-providers-detail", kwargs={"pk": provider_id}),
|
||||
data=patch_payload,
|
||||
content_type=API_JSON_CONTENT_TYPE,
|
||||
)
|
||||
assert patch_resp.status_code == status.HTTP_200_OK
|
||||
updated = patch_resp.json()["data"]["attributes"]
|
||||
assert updated["base_url"] == "https://api.example.com/v1"
|
||||
assert updated["is_active"] is False
|
||||
|
||||
def test_openai_patch_invalid_credentials(self, authenticated_client):
|
||||
"""PATCH with invalid credentials.api_key should error (400)"""
|
||||
valid_key = "sk-ok123T3BlbkFJok456"
|
||||
create_payload = {
|
||||
"data": {
|
||||
"type": "lighthouse-providers",
|
||||
"attributes": {
|
||||
"provider_type": "openai",
|
||||
"credentials": {"api_key": valid_key},
|
||||
},
|
||||
}
|
||||
}
|
||||
create_resp = authenticated_client.post(
|
||||
reverse("lighthouse-providers-list"),
|
||||
data=create_payload,
|
||||
content_type=API_JSON_CONTENT_TYPE,
|
||||
)
|
||||
assert create_resp.status_code == status.HTTP_201_CREATED
|
||||
provider_id = create_resp.json()["data"]["id"]
|
||||
|
||||
# Try patch with invalid api_key format
|
||||
patch_payload = {
|
||||
"data": {
|
||||
"type": "lighthouse-providers",
|
||||
"id": provider_id,
|
||||
"attributes": {
|
||||
"credentials": {"api_key": "ks-invalid-format"},
|
||||
},
|
||||
}
|
||||
}
|
||||
patch_resp = authenticated_client.patch(
|
||||
reverse("lighthouse-providers-detail", kwargs={"pk": provider_id}),
|
||||
data=patch_payload,
|
||||
content_type=API_JSON_CONTENT_TYPE,
|
||||
)
|
||||
assert patch_resp.status_code == status.HTTP_400_BAD_REQUEST
|
||||
|
||||
def test_openai_get_masking_and_fields_filter(self, authenticated_client):
|
||||
valid_key = "sk-get123T3BlbkFJget456"
|
||||
create_payload = {
|
||||
"data": {
|
||||
"type": "lighthouse-providers",
|
||||
"attributes": {
|
||||
"provider_type": "openai",
|
||||
"credentials": {"api_key": valid_key},
|
||||
},
|
||||
}
|
||||
}
|
||||
create_resp = authenticated_client.post(
|
||||
reverse("lighthouse-providers-list"),
|
||||
data=create_payload,
|
||||
content_type=API_JSON_CONTENT_TYPE,
|
||||
)
|
||||
assert create_resp.status_code == status.HTTP_201_CREATED
|
||||
provider_id = create_resp.json()["data"]["id"]
|
||||
|
||||
# Default GET should return masked credentials
|
||||
get_resp = authenticated_client.get(
|
||||
reverse("lighthouse-providers-detail", kwargs={"pk": provider_id})
|
||||
)
|
||||
assert get_resp.status_code == status.HTTP_200_OK
|
||||
masked = get_resp.json()["data"]["attributes"]["credentials"]["api_key"]
|
||||
assert masked == ("*" * len(valid_key))
|
||||
|
||||
# Fields filter should return decrypted credentials structure
|
||||
get_full = authenticated_client.get(
|
||||
reverse("lighthouse-providers-detail", kwargs={"pk": provider_id})
|
||||
+ "?fields[lighthouse-providers]=credentials"
|
||||
)
|
||||
assert get_full.status_code == status.HTTP_200_OK
|
||||
creds = get_full.json()["data"]["attributes"]["credentials"]
|
||||
assert creds["api_key"] == valid_key
|
||||
|
||||
def test_delete_provider_updates_tenant_defaults(
|
||||
self, authenticated_client, tenants_fixture
|
||||
):
|
||||
"""Deleting a provider config should clear tenant default_provider and its default_model entry."""
|
||||
|
||||
tenant = tenants_fixture[0]
|
||||
|
||||
# Create provider configuration to delete
|
||||
provider = LighthouseProviderConfiguration.objects.create(
|
||||
tenant_id=tenant.id,
|
||||
provider_type="openai",
|
||||
credentials=b'{"api_key":"sk-test123T3BlbkFJ"}',
|
||||
is_active=True,
|
||||
)
|
||||
|
||||
# Seed tenant defaults referencing the provider we will delete
|
||||
cfg = LighthouseTenantConfiguration.objects.create(
|
||||
tenant_id=tenant.id,
|
||||
business_context="Test",
|
||||
default_provider="openai",
|
||||
default_models={"openai": "gpt-4o", "other": "model-x"},
|
||||
)
|
||||
|
||||
# Delete via API and validate response
|
||||
url = reverse("lighthouse-providers-detail", kwargs={"pk": str(provider.id)})
|
||||
resp = authenticated_client.delete(url)
|
||||
assert resp.status_code in (
|
||||
status.HTTP_204_NO_CONTENT,
|
||||
status.HTTP_200_OK,
|
||||
)
|
||||
|
||||
# Tenant defaults should be updated
|
||||
cfg.refresh_from_db()
|
||||
assert cfg.default_provider == ""
|
||||
assert "openai" not in cfg.default_models
|
||||
|
||||
# Unrelated entries should remain untouched
|
||||
assert cfg.default_models.get("other") == "model-x"
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
import re
|
||||
|
||||
from rest_framework_json_api import serializers
|
||||
|
||||
|
||||
class OpenAICredentialsSerializer(serializers.Serializer):
|
||||
api_key = serializers.CharField()
|
||||
|
||||
def validate_api_key(self, value: str) -> str:
|
||||
pattern = r"^sk-[\w-]+$"
|
||||
if not re.match(pattern, value or ""):
|
||||
raise serializers.ValidationError("Invalid OpenAI API key format.")
|
||||
return value
|
||||
@@ -6,10 +6,8 @@ from django.conf import settings
|
||||
from django.contrib.auth import authenticate
|
||||
from django.contrib.auth.models import update_last_login
|
||||
from django.contrib.auth.password_validation import validate_password
|
||||
from django.db import IntegrityError
|
||||
from drf_spectacular.utils import extend_schema_field
|
||||
from jwt.exceptions import InvalidKeyError
|
||||
from rest_framework.reverse import reverse
|
||||
from rest_framework.validators import UniqueTogetherValidator
|
||||
from rest_framework_json_api import serializers
|
||||
from rest_framework_json_api.relations import SerializerMethodResourceRelatedField
|
||||
@@ -27,9 +25,6 @@ from api.models import (
|
||||
Invitation,
|
||||
InvitationRoleRelationship,
|
||||
LighthouseConfiguration,
|
||||
LighthouseProviderConfiguration,
|
||||
LighthouseProviderModels,
|
||||
LighthouseTenantConfiguration,
|
||||
Membership,
|
||||
Processor,
|
||||
Provider,
|
||||
@@ -59,7 +54,6 @@ from api.v1.serializer_utils.integrations import (
|
||||
S3ConfigSerializer,
|
||||
SecurityHubConfigSerializer,
|
||||
)
|
||||
from api.v1.serializer_utils.lighthouse import OpenAICredentialsSerializer
|
||||
from api.v1.serializer_utils.processors import ProcessorConfigField
|
||||
from api.v1.serializer_utils.providers import ProviderSecretField
|
||||
from prowler.lib.mutelist.mutelist import Mutelist
|
||||
@@ -2756,16 +2750,6 @@ class LighthouseConfigCreateSerializer(RLSSerializer, BaseWriteSerializer):
|
||||
"updated_at": {"read_only": True},
|
||||
}
|
||||
|
||||
def validate_temperature(self, value):
|
||||
if not 0 <= value <= 1:
|
||||
raise ValidationError("Temperature must be between 0 and 1.")
|
||||
return value
|
||||
|
||||
def validate_max_tokens(self, value):
|
||||
if not 500 <= value <= 5000:
|
||||
raise ValidationError("Max tokens must be between 500 and 5000.")
|
||||
return value
|
||||
|
||||
def validate(self, attrs):
|
||||
tenant_id = self.context.get("request").tenant_id
|
||||
if LighthouseConfiguration.objects.filter(tenant_id=tenant_id).exists():
|
||||
@@ -2774,11 +2758,6 @@ class LighthouseConfigCreateSerializer(RLSSerializer, BaseWriteSerializer):
|
||||
"tenant_id": "Lighthouse configuration already exists for this tenant."
|
||||
}
|
||||
)
|
||||
api_key = attrs.get("api_key")
|
||||
if api_key is not None:
|
||||
OpenAICredentialsSerializer(data={"api_key": api_key}).is_valid(
|
||||
raise_exception=True
|
||||
)
|
||||
return super().validate(attrs)
|
||||
|
||||
def create(self, validated_data):
|
||||
@@ -2823,24 +2802,6 @@ class LighthouseConfigUpdateSerializer(BaseWriteSerializer):
|
||||
"max_tokens": {"required": False},
|
||||
}
|
||||
|
||||
def validate_temperature(self, value):
|
||||
if not 0 <= value <= 1:
|
||||
raise ValidationError("Temperature must be between 0 and 1.")
|
||||
return value
|
||||
|
||||
def validate_max_tokens(self, value):
|
||||
if not 500 <= value <= 5000:
|
||||
raise ValidationError("Max tokens must be between 500 and 5000.")
|
||||
return value
|
||||
|
||||
def validate(self, attrs):
|
||||
api_key = attrs.get("api_key", None)
|
||||
if api_key is not None:
|
||||
OpenAICredentialsSerializer(data={"api_key": api_key}).is_valid(
|
||||
raise_exception=True
|
||||
)
|
||||
return super().validate(attrs)
|
||||
|
||||
def update(self, instance, validated_data):
|
||||
api_key = validated_data.pop("api_key", None)
|
||||
instance = super().update(instance, validated_data)
|
||||
@@ -2970,352 +2931,3 @@ class TenantApiKeyUpdateSerializer(RLSSerializer, BaseWriteSerializer):
|
||||
):
|
||||
raise ValidationError("An API key with this name already exists.")
|
||||
return value
|
||||
|
||||
|
||||
# Lighthouse: Provider configurations
|
||||
|
||||
|
||||
class LighthouseProviderConfigSerializer(RLSSerializer):
|
||||
"""
|
||||
Read serializer for LighthouseProviderConfiguration.
|
||||
"""
|
||||
|
||||
# Decrypted credentials are only returned in to_representation when requested
|
||||
credentials = serializers.JSONField(required=False, read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = LighthouseProviderConfiguration
|
||||
fields = [
|
||||
"id",
|
||||
"inserted_at",
|
||||
"updated_at",
|
||||
"provider_type",
|
||||
"base_url",
|
||||
"is_active",
|
||||
"credentials",
|
||||
"url",
|
||||
]
|
||||
extra_kwargs = {
|
||||
"id": {"read_only": True},
|
||||
"inserted_at": {"read_only": True},
|
||||
"updated_at": {"read_only": True},
|
||||
"is_active": {"read_only": True},
|
||||
"url": {"read_only": True, "view_name": "lighthouse-providers-detail"},
|
||||
}
|
||||
|
||||
class JSONAPIMeta:
|
||||
resource_name = "lighthouse-providers"
|
||||
|
||||
def to_representation(self, instance):
|
||||
data = super().to_representation(instance)
|
||||
# Support JSON:API fields filter: fields[lighthouse-providers]=credentials,base_url
|
||||
fields_param = self.context.get("request", None) and self.context[
|
||||
"request"
|
||||
].query_params.get("fields[lighthouse-providers]", "")
|
||||
|
||||
creds = instance.credentials_decoded
|
||||
|
||||
requested_fields = (
|
||||
[f.strip() for f in fields_param.split(",")] if fields_param else []
|
||||
)
|
||||
|
||||
if "credentials" in requested_fields:
|
||||
# Return full decrypted credentials JSON
|
||||
data["credentials"] = creds
|
||||
else:
|
||||
# Return masked credentials by default
|
||||
def mask_value(value):
|
||||
if isinstance(value, str):
|
||||
return "*" * len(value)
|
||||
if isinstance(value, dict):
|
||||
return {k: mask_value(v) for k, v in value.items()}
|
||||
if isinstance(value, list):
|
||||
return [mask_value(v) for v in value]
|
||||
return value
|
||||
|
||||
# Always return masked credentials, even if creds is None
|
||||
if creds is not None:
|
||||
data["credentials"] = mask_value(creds)
|
||||
else:
|
||||
# If credentials_decoded returns None, return None for credentials field
|
||||
data["credentials"] = None
|
||||
|
||||
return data
|
||||
|
||||
|
||||
class LighthouseProviderConfigCreateSerializer(RLSSerializer, BaseWriteSerializer):
|
||||
"""
|
||||
Create serializer for LighthouseProviderConfiguration.
|
||||
Accepts credentials as JSON; stored encrypted via credentials_decoded.
|
||||
"""
|
||||
|
||||
credentials = serializers.JSONField(write_only=True, required=True)
|
||||
|
||||
class Meta:
|
||||
model = LighthouseProviderConfiguration
|
||||
fields = [
|
||||
"provider_type",
|
||||
"base_url",
|
||||
"credentials",
|
||||
"is_active",
|
||||
]
|
||||
extra_kwargs = {
|
||||
"is_active": {"required": False},
|
||||
"base_url": {"required": False, "allow_null": True},
|
||||
}
|
||||
|
||||
def create(self, validated_data):
|
||||
credentials = validated_data.pop("credentials")
|
||||
|
||||
instance = LighthouseProviderConfiguration(**validated_data)
|
||||
instance.tenant_id = self.context.get("tenant_id")
|
||||
instance.credentials_decoded = credentials
|
||||
|
||||
try:
|
||||
instance.save()
|
||||
return instance
|
||||
except IntegrityError:
|
||||
raise ValidationError(
|
||||
{
|
||||
"provider_type": "Configuration for this provider already exists for the tenant."
|
||||
}
|
||||
)
|
||||
|
||||
def validate(self, attrs):
|
||||
provider_type = attrs.get("provider_type")
|
||||
credentials = attrs.get("credentials") or {}
|
||||
|
||||
if provider_type == LighthouseProviderConfiguration.LLMProviderChoices.OPENAI:
|
||||
try:
|
||||
OpenAICredentialsSerializer(data=credentials).is_valid(
|
||||
raise_exception=True
|
||||
)
|
||||
except ValidationError as e:
|
||||
details = e.detail.copy()
|
||||
for key, value in details.items():
|
||||
e.detail[f"credentials/{key}"] = value
|
||||
del e.detail[key]
|
||||
raise e
|
||||
|
||||
return super().validate(attrs)
|
||||
|
||||
|
||||
class LighthouseProviderConfigUpdateSerializer(BaseWriteSerializer):
|
||||
"""
|
||||
Update serializer for LighthouseProviderConfiguration.
|
||||
"""
|
||||
|
||||
credentials = serializers.JSONField(write_only=True, required=False)
|
||||
|
||||
class Meta:
|
||||
model = LighthouseProviderConfiguration
|
||||
fields = [
|
||||
"id",
|
||||
"provider_type",
|
||||
"base_url",
|
||||
"credentials",
|
||||
"is_active",
|
||||
]
|
||||
extra_kwargs = {
|
||||
"id": {"read_only": True},
|
||||
"provider_type": {"read_only": True},
|
||||
"base_url": {"required": False, "allow_null": True},
|
||||
"is_active": {"required": False},
|
||||
}
|
||||
|
||||
def update(self, instance, validated_data):
|
||||
credentials = validated_data.pop("credentials", None)
|
||||
|
||||
for attr, value in validated_data.items():
|
||||
setattr(instance, attr, value)
|
||||
|
||||
if credentials is not None:
|
||||
instance.credentials_decoded = credentials
|
||||
|
||||
instance.save()
|
||||
return instance
|
||||
|
||||
def validate(self, attrs):
|
||||
provider_type = getattr(self.instance, "provider_type", None)
|
||||
credentials = attrs.get("credentials", None)
|
||||
|
||||
if (
|
||||
credentials is not None
|
||||
and provider_type
|
||||
== LighthouseProviderConfiguration.LLMProviderChoices.OPENAI
|
||||
):
|
||||
try:
|
||||
OpenAICredentialsSerializer(data=credentials).is_valid(
|
||||
raise_exception=True
|
||||
)
|
||||
except ValidationError as e:
|
||||
details = e.detail.copy()
|
||||
for key, value in details.items():
|
||||
e.detail[f"credentials/{key}"] = value
|
||||
del e.detail[key]
|
||||
raise e
|
||||
|
||||
return super().validate(attrs)
|
||||
|
||||
|
||||
# Lighthouse: Tenant configuration
|
||||
|
||||
|
||||
class LighthouseTenantConfigSerializer(RLSSerializer):
|
||||
"""
|
||||
Read serializer for LighthouseTenantConfiguration.
|
||||
"""
|
||||
|
||||
# Build singleton URL without pk
|
||||
url = serializers.SerializerMethodField()
|
||||
|
||||
def get_url(self, obj):
|
||||
request = self.context.get("request")
|
||||
return reverse("lighthouse-config", request=request)
|
||||
|
||||
class Meta:
|
||||
model = LighthouseTenantConfiguration
|
||||
fields = [
|
||||
"id",
|
||||
"inserted_at",
|
||||
"updated_at",
|
||||
"business_context",
|
||||
"default_provider",
|
||||
"default_models",
|
||||
"url",
|
||||
]
|
||||
extra_kwargs = {
|
||||
"id": {"read_only": True},
|
||||
"inserted_at": {"read_only": True},
|
||||
"updated_at": {"read_only": True},
|
||||
"url": {"read_only": True},
|
||||
}
|
||||
|
||||
|
||||
class LighthouseTenantConfigUpdateSerializer(BaseWriteSerializer):
|
||||
class Meta:
|
||||
model = LighthouseTenantConfiguration
|
||||
fields = [
|
||||
"id",
|
||||
"business_context",
|
||||
"default_provider",
|
||||
"default_models",
|
||||
]
|
||||
extra_kwargs = {
|
||||
"id": {"read_only": True},
|
||||
}
|
||||
|
||||
def validate(self, attrs):
|
||||
request = self.context.get("request")
|
||||
tenant_id = self.context.get("tenant_id") or (
|
||||
getattr(request, "tenant_id", None) if request else None
|
||||
)
|
||||
|
||||
default_provider = attrs.get(
|
||||
"default_provider", getattr(self.instance, "default_provider", "")
|
||||
)
|
||||
default_models = attrs.get(
|
||||
"default_models", getattr(self.instance, "default_models", {})
|
||||
)
|
||||
|
||||
if default_provider:
|
||||
supported = set(LighthouseProviderConfiguration.LLMProviderChoices.values)
|
||||
if default_provider not in supported:
|
||||
raise ValidationError(
|
||||
{"default_provider": f"Unsupported provider '{default_provider}'."}
|
||||
)
|
||||
if not LighthouseProviderConfiguration.objects.filter(
|
||||
tenant_id=tenant_id, provider_type=default_provider, is_active=True
|
||||
).exists():
|
||||
raise ValidationError(
|
||||
{
|
||||
"default_provider": f"No active configuration found for '{default_provider}'."
|
||||
}
|
||||
)
|
||||
|
||||
if default_models is not None and not isinstance(default_models, dict):
|
||||
raise ValidationError(
|
||||
{"default_models": "Must be an object mapping provider -> model_id."}
|
||||
)
|
||||
|
||||
for provider_type, model_id in (default_models or {}).items():
|
||||
provider_cfg = LighthouseProviderConfiguration.objects.filter(
|
||||
tenant_id=tenant_id, provider_type=provider_type, is_active=True
|
||||
).first()
|
||||
if not provider_cfg:
|
||||
raise ValidationError(
|
||||
{
|
||||
"default_models": f"No active configuration for provider '{provider_type}'."
|
||||
}
|
||||
)
|
||||
if not LighthouseProviderModels.objects.filter(
|
||||
tenant_id=tenant_id,
|
||||
provider_configuration=provider_cfg,
|
||||
model_id=model_id,
|
||||
).exists():
|
||||
raise ValidationError(
|
||||
{
|
||||
"default_models": f"Invalid model '{model_id}' for provider '{provider_type}'."
|
||||
}
|
||||
)
|
||||
|
||||
return super().validate(attrs)
|
||||
|
||||
|
||||
# Lighthouse: Provider models
|
||||
|
||||
|
||||
class LighthouseProviderModelsSerializer(RLSSerializer):
|
||||
"""
|
||||
Read serializer for LighthouseProviderModels.
|
||||
"""
|
||||
|
||||
provider_configuration = serializers.ResourceRelatedField(read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = LighthouseProviderModels
|
||||
fields = [
|
||||
"id",
|
||||
"inserted_at",
|
||||
"updated_at",
|
||||
"provider_configuration",
|
||||
"model_id",
|
||||
"model_name",
|
||||
"default_parameters",
|
||||
"url",
|
||||
]
|
||||
extra_kwargs = {
|
||||
"id": {"read_only": True},
|
||||
"inserted_at": {"read_only": True},
|
||||
"updated_at": {"read_only": True},
|
||||
"url": {"read_only": True, "view_name": "lighthouse-models-detail"},
|
||||
}
|
||||
|
||||
|
||||
class LighthouseProviderModelsCreateSerializer(RLSSerializer, BaseWriteSerializer):
|
||||
provider_configuration = serializers.ResourceRelatedField(
|
||||
queryset=LighthouseProviderConfiguration.objects.all()
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = LighthouseProviderModels
|
||||
fields = [
|
||||
"provider_configuration",
|
||||
"model_id",
|
||||
"default_parameters",
|
||||
]
|
||||
extra_kwargs = {
|
||||
"default_parameters": {"required": False},
|
||||
}
|
||||
|
||||
|
||||
class LighthouseProviderModelsUpdateSerializer(BaseWriteSerializer):
|
||||
class Meta:
|
||||
model = LighthouseProviderModels
|
||||
fields = [
|
||||
"id",
|
||||
"default_parameters",
|
||||
]
|
||||
extra_kwargs = {
|
||||
"id": {"read_only": True},
|
||||
}
|
||||
|
||||
@@ -17,9 +17,6 @@ from api.v1.views import (
|
||||
InvitationAcceptViewSet,
|
||||
InvitationViewSet,
|
||||
LighthouseConfigViewSet,
|
||||
LighthouseProviderConfigViewSet,
|
||||
LighthouseProviderModelsViewSet,
|
||||
LighthouseTenantConfigViewSet,
|
||||
MembershipViewSet,
|
||||
OverviewViewSet,
|
||||
ProcessorViewSet,
|
||||
@@ -37,12 +34,12 @@ from api.v1.views import (
|
||||
ScheduleViewSet,
|
||||
SchemaView,
|
||||
TaskViewSet,
|
||||
TenantApiKeyViewSet,
|
||||
TenantFinishACSView,
|
||||
TenantMembersViewSet,
|
||||
TenantViewSet,
|
||||
UserRoleRelationshipView,
|
||||
UserViewSet,
|
||||
TenantApiKeyViewSet,
|
||||
)
|
||||
|
||||
router = routers.DefaultRouter(trailing_slash=False)
|
||||
@@ -70,16 +67,6 @@ router.register(
|
||||
basename="lighthouseconfiguration",
|
||||
)
|
||||
router.register(r"api-keys", TenantApiKeyViewSet, basename="api-key")
|
||||
router.register(
|
||||
r"lighthouse/providers",
|
||||
LighthouseProviderConfigViewSet,
|
||||
basename="lighthouse-providers",
|
||||
)
|
||||
router.register(
|
||||
r"lighthouse/models",
|
||||
LighthouseProviderModelsViewSet,
|
||||
basename="lighthouse-models",
|
||||
)
|
||||
|
||||
tenants_router = routers.NestedSimpleRouter(router, r"tenants", lookup="tenant")
|
||||
tenants_router.register(
|
||||
@@ -150,14 +137,6 @@ urlpatterns = [
|
||||
),
|
||||
name="provider_group-providers-relationship",
|
||||
),
|
||||
# Lighthouse tenant config as singleton endpoint
|
||||
path(
|
||||
"lighthouse/configuration",
|
||||
LighthouseTenantConfigViewSet.as_view(
|
||||
{"get": "list", "patch": "partial_update"}
|
||||
),
|
||||
name="lighthouse-config",
|
||||
),
|
||||
# API endpoint to start SAML SSO flow
|
||||
path(
|
||||
"auth/saml/initiate/", SAMLInitiateAPIView.as_view(), name="api_saml_initiate"
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import fnmatch
|
||||
import glob
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
from datetime import datetime, timedelta, timezone
|
||||
@@ -61,13 +60,11 @@ from tasks.tasks import (
|
||||
backfill_scan_resource_summaries_task,
|
||||
check_integration_connection_task,
|
||||
check_lighthouse_connection_task,
|
||||
check_lighthouse_provider_connection_task,
|
||||
check_provider_connection_task,
|
||||
delete_provider_task,
|
||||
delete_tenant_task,
|
||||
jira_integration_task,
|
||||
perform_scan_task,
|
||||
refresh_lighthouse_provider_models_task,
|
||||
)
|
||||
|
||||
from api.base_views import BaseRLSViewSet, BaseTenantViewset, BaseUserViewset
|
||||
@@ -87,8 +84,6 @@ from api.filters import (
|
||||
InvitationFilter,
|
||||
LatestFindingFilter,
|
||||
LatestResourceFilter,
|
||||
LighthouseProviderConfigFilter,
|
||||
LighthouseProviderModelsFilter,
|
||||
MembershipFilter,
|
||||
ProcessorFilter,
|
||||
ProviderFilter,
|
||||
@@ -111,9 +106,6 @@ from api.models import (
|
||||
Integration,
|
||||
Invitation,
|
||||
LighthouseConfiguration,
|
||||
LighthouseProviderConfiguration,
|
||||
LighthouseProviderModels,
|
||||
LighthouseTenantConfiguration,
|
||||
Membership,
|
||||
Processor,
|
||||
Provider,
|
||||
@@ -168,12 +160,6 @@ from api.v1.serializers import (
|
||||
LighthouseConfigCreateSerializer,
|
||||
LighthouseConfigSerializer,
|
||||
LighthouseConfigUpdateSerializer,
|
||||
LighthouseProviderConfigCreateSerializer,
|
||||
LighthouseProviderConfigSerializer,
|
||||
LighthouseProviderConfigUpdateSerializer,
|
||||
LighthouseProviderModelsSerializer,
|
||||
LighthouseTenantConfigSerializer,
|
||||
LighthouseTenantConfigUpdateSerializer,
|
||||
MembershipSerializer,
|
||||
OverviewFindingSerializer,
|
||||
OverviewProviderSerializer,
|
||||
@@ -321,7 +307,7 @@ class SchemaView(SpectacularAPIView):
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
spectacular_settings.TITLE = "Prowler API"
|
||||
spectacular_settings.VERSION = "1.15.0"
|
||||
spectacular_settings.VERSION = "1.14.0"
|
||||
spectacular_settings.DESCRIPTION = (
|
||||
"Prowler API specification.\n\nThis file is auto-generated."
|
||||
)
|
||||
@@ -4191,25 +4177,21 @@ class IntegrationJiraViewSet(BaseRLSViewSet):
|
||||
tags=["Lighthouse AI"],
|
||||
summary="List all Lighthouse AI configurations",
|
||||
description="Retrieve a list of all Lighthouse AI configurations.",
|
||||
deprecated=True,
|
||||
),
|
||||
create=extend_schema(
|
||||
tags=["Lighthouse AI"],
|
||||
summary="Create a new Lighthouse AI configuration",
|
||||
description="Create a new Lighthouse AI configuration with the specified details.",
|
||||
deprecated=True,
|
||||
),
|
||||
partial_update=extend_schema(
|
||||
tags=["Lighthouse AI"],
|
||||
summary="Partially update a Lighthouse AI configuration",
|
||||
description="Update certain fields of an existing Lighthouse AI configuration.",
|
||||
deprecated=True,
|
||||
),
|
||||
destroy=extend_schema(
|
||||
tags=["Lighthouse AI"],
|
||||
summary="Delete a Lighthouse AI configuration",
|
||||
description="Remove a Lighthouse AI configuration by its ID.",
|
||||
deprecated=True,
|
||||
),
|
||||
connection=extend_schema(
|
||||
tags=["Lighthouse AI"],
|
||||
@@ -4217,7 +4199,6 @@ class IntegrationJiraViewSet(BaseRLSViewSet):
|
||||
description="Verify the connection to the OpenAI API for a specific Lighthouse AI configuration.",
|
||||
request=None,
|
||||
responses={202: OpenApiResponse(response=TaskSerializer)},
|
||||
deprecated=True,
|
||||
),
|
||||
)
|
||||
class LighthouseConfigViewSet(BaseRLSViewSet):
|
||||
@@ -4268,273 +4249,6 @@ class LighthouseConfigViewSet(BaseRLSViewSet):
|
||||
)
|
||||
|
||||
|
||||
@extend_schema_view(
|
||||
list=extend_schema(
|
||||
tags=["Lighthouse AI"],
|
||||
summary="List all LLM provider configs",
|
||||
description="Retrieve all LLM provider configurations for the current tenant",
|
||||
),
|
||||
retrieve=extend_schema(
|
||||
tags=["Lighthouse AI"],
|
||||
summary="Retrieve LLM provider config",
|
||||
description="Get details for a specific provider configuration in the current tenant.",
|
||||
),
|
||||
create=extend_schema(
|
||||
tags=["Lighthouse AI"],
|
||||
summary="Create LLM provider config",
|
||||
description="Create a per-tenant configuration for an LLM provider. Only one configuration per provider type is allowed per tenant.",
|
||||
),
|
||||
partial_update=extend_schema(
|
||||
tags=["Lighthouse AI"],
|
||||
summary="Update LLM provider config",
|
||||
description="Partially update a provider configuration (e.g., base_url, is_active).",
|
||||
),
|
||||
destroy=extend_schema(
|
||||
tags=["Lighthouse AI"],
|
||||
summary="Delete LLM provider config",
|
||||
description="Delete a provider configuration. Any tenant defaults that reference this provider are cleared during deletion.",
|
||||
),
|
||||
)
|
||||
class LighthouseProviderConfigViewSet(BaseRLSViewSet):
|
||||
queryset = LighthouseProviderConfiguration.objects.all()
|
||||
serializer_class = LighthouseProviderConfigSerializer
|
||||
http_method_names = ["get", "post", "patch", "delete"]
|
||||
filterset_class = LighthouseProviderConfigFilter
|
||||
|
||||
def get_queryset(self):
|
||||
if getattr(self, "swagger_fake_view", False):
|
||||
return LighthouseProviderConfiguration.objects.none()
|
||||
return LighthouseProviderConfiguration.objects.filter(
|
||||
tenant_id=self.request.tenant_id
|
||||
)
|
||||
|
||||
def get_serializer_class(self):
|
||||
if self.action == "create":
|
||||
return LighthouseProviderConfigCreateSerializer
|
||||
elif self.action == "partial_update":
|
||||
return LighthouseProviderConfigUpdateSerializer
|
||||
elif self.action in ["connection", "refresh_models"]:
|
||||
return TaskSerializer
|
||||
return super().get_serializer_class()
|
||||
|
||||
def create(self, request, *args, **kwargs):
|
||||
serializer = self.get_serializer(data=request.data)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
instance = serializer.save()
|
||||
|
||||
read_serializer = LighthouseProviderConfigSerializer(
|
||||
instance, context=self.get_serializer_context()
|
||||
)
|
||||
headers = self.get_success_headers(read_serializer.data)
|
||||
return Response(
|
||||
data=read_serializer.data,
|
||||
status=status.HTTP_201_CREATED,
|
||||
headers=headers,
|
||||
)
|
||||
|
||||
def partial_update(self, request, *args, **kwargs):
|
||||
instance = self.get_object()
|
||||
serializer = self.get_serializer(
|
||||
instance,
|
||||
data=request.data,
|
||||
partial=True,
|
||||
context=self.get_serializer_context(),
|
||||
)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
serializer.save()
|
||||
read_serializer = LighthouseProviderConfigSerializer(
|
||||
instance, context=self.get_serializer_context()
|
||||
)
|
||||
return Response(data=read_serializer.data, status=status.HTTP_200_OK)
|
||||
|
||||
@extend_schema(
|
||||
tags=["Lighthouse AI"],
|
||||
summary="Check LLM provider connection",
|
||||
description="Validate provider credentials asynchronously and toggle is_active.",
|
||||
request=None,
|
||||
responses={202: OpenApiResponse(response=TaskSerializer)},
|
||||
)
|
||||
@action(detail=True, methods=["post"], url_name="connection")
|
||||
def connection(self, request, pk=None):
|
||||
instance = self.get_object()
|
||||
if (
|
||||
instance.provider_type
|
||||
!= LighthouseProviderConfiguration.LLMProviderChoices.OPENAI
|
||||
):
|
||||
return Response(
|
||||
data={
|
||||
"errors": [{"detail": "Only 'openai' provider supported in MVP"}]
|
||||
},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
with transaction.atomic():
|
||||
task = check_lighthouse_provider_connection_task.delay(
|
||||
provider_config_id=str(instance.id), tenant_id=self.request.tenant_id
|
||||
)
|
||||
|
||||
prowler_task = Task.objects.get(id=task.id)
|
||||
serializer = TaskSerializer(prowler_task)
|
||||
return Response(
|
||||
data=serializer.data,
|
||||
status=status.HTTP_202_ACCEPTED,
|
||||
headers={
|
||||
"Content-Location": reverse(
|
||||
"task-detail", kwargs={"pk": prowler_task.id}
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
@extend_schema(
|
||||
tags=["Lighthouse AI"],
|
||||
summary="Refresh LLM models catalog",
|
||||
description="Fetch available models for this provider configuration and upsert into catalog.",
|
||||
request=None,
|
||||
responses={202: OpenApiResponse(response=TaskSerializer)},
|
||||
)
|
||||
@action(
|
||||
detail=True,
|
||||
methods=["post"],
|
||||
url_path="refresh-models",
|
||||
url_name="refresh-models",
|
||||
)
|
||||
def refresh_models(self, request, pk=None):
|
||||
instance = self.get_object()
|
||||
if (
|
||||
instance.provider_type
|
||||
!= LighthouseProviderConfiguration.LLMProviderChoices.OPENAI
|
||||
):
|
||||
return Response(
|
||||
data={
|
||||
"errors": [{"detail": "Only 'openai' provider supported in MVP"}]
|
||||
},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
with transaction.atomic():
|
||||
task = refresh_lighthouse_provider_models_task.delay(
|
||||
provider_config_id=str(instance.id), tenant_id=self.request.tenant_id
|
||||
)
|
||||
|
||||
prowler_task = Task.objects.get(id=task.id)
|
||||
serializer = TaskSerializer(prowler_task)
|
||||
return Response(
|
||||
data=serializer.data,
|
||||
status=status.HTTP_202_ACCEPTED,
|
||||
headers={
|
||||
"Content-Location": reverse(
|
||||
"task-detail", kwargs={"pk": prowler_task.id}
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@extend_schema_view(
|
||||
list=extend_schema(
|
||||
tags=["Lighthouse AI"],
|
||||
summary="Get Lighthouse AI Tenant config",
|
||||
description="Retrieve current tenant-level Lighthouse AI settings. Returns a single configuration object.",
|
||||
),
|
||||
partial_update=extend_schema(
|
||||
tags=["Lighthouse AI"],
|
||||
summary="Update Lighthouse AI Tenant config",
|
||||
description="Update tenant-level settings. Validates that the default provider is configured and active and that default model IDs exist for the chosen providers. Auto-creates configuration if it doesn't exist.",
|
||||
),
|
||||
)
|
||||
class LighthouseTenantConfigViewSet(BaseRLSViewSet):
|
||||
"""
|
||||
Singleton endpoint for tenant-level Lighthouse AI configuration.
|
||||
|
||||
This viewset implements a true singleton pattern:
|
||||
- GET returns the single configuration object (or 404 if not found)
|
||||
- PATCH updates/creates the configuration (upsert semantics)
|
||||
- No ID is required in the URL
|
||||
"""
|
||||
|
||||
queryset = LighthouseTenantConfiguration.objects.all()
|
||||
serializer_class = LighthouseTenantConfigSerializer
|
||||
http_method_names = ["get", "patch"]
|
||||
|
||||
def get_queryset(self):
|
||||
if getattr(self, "swagger_fake_view", False):
|
||||
return LighthouseTenantConfiguration.objects.none()
|
||||
return LighthouseTenantConfiguration.objects.filter(
|
||||
tenant_id=self.request.tenant_id
|
||||
)
|
||||
|
||||
def get_serializer_class(self):
|
||||
if self.action == "partial_update":
|
||||
return LighthouseTenantConfigUpdateSerializer
|
||||
return super().get_serializer_class()
|
||||
|
||||
def get_object(self):
|
||||
"""Retrieve the singleton instance for the current tenant."""
|
||||
obj = LighthouseTenantConfiguration.objects.filter(
|
||||
tenant_id=self.request.tenant_id
|
||||
).first()
|
||||
if obj is None:
|
||||
raise NotFound("Tenant Lighthouse configuration not found")
|
||||
self.check_object_permissions(self.request, obj)
|
||||
return obj
|
||||
|
||||
def list(self, request, *args, **kwargs):
|
||||
"""GET endpoint for singleton - returns single object, not an array."""
|
||||
instance = self.get_object()
|
||||
serializer = self.get_serializer(instance)
|
||||
return Response(serializer.data)
|
||||
|
||||
def partial_update(self, request, *args, **kwargs):
|
||||
"""PATCH endpoint for singleton - no pk required. Auto-creates if not exists."""
|
||||
# Auto-create tenant config if it doesn't exist (upsert semantics)
|
||||
instance, created = LighthouseTenantConfiguration.objects.get_or_create(
|
||||
tenant_id=self.request.tenant_id,
|
||||
defaults={},
|
||||
)
|
||||
|
||||
# Extract attributes from JSON:API payload
|
||||
try:
|
||||
payload = json.loads(request.body)
|
||||
attributes = payload.get("data", {}).get("attributes", {})
|
||||
except (json.JSONDecodeError, AttributeError):
|
||||
raise ValidationError("Invalid JSON:API payload")
|
||||
|
||||
serializer = self.get_serializer(instance, data=attributes, partial=True)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
serializer.save()
|
||||
read_serializer = LighthouseTenantConfigSerializer(
|
||||
instance, context=self.get_serializer_context()
|
||||
)
|
||||
return Response(read_serializer.data, status=status.HTTP_200_OK)
|
||||
|
||||
|
||||
@extend_schema_view(
|
||||
list=extend_schema(
|
||||
tags=["Lighthouse AI"],
|
||||
summary="List all LLM models",
|
||||
description="List available LLM models per configured provider for the current tenant.",
|
||||
),
|
||||
retrieve=extend_schema(
|
||||
tags=["Lighthouse AI"],
|
||||
summary="Retrieve LLM model details",
|
||||
description="Get details for a specific LLM model.",
|
||||
),
|
||||
)
|
||||
class LighthouseProviderModelsViewSet(BaseRLSViewSet):
|
||||
queryset = LighthouseProviderModels.objects.all()
|
||||
serializer_class = LighthouseProviderModelsSerializer
|
||||
filterset_class = LighthouseProviderModelsFilter
|
||||
# Expose as read-only catalog collection
|
||||
http_method_names = ["get"]
|
||||
|
||||
def get_queryset(self):
|
||||
if getattr(self, "swagger_fake_view", False):
|
||||
return LighthouseProviderModels.objects.none()
|
||||
return LighthouseProviderModels.objects.filter(tenant_id=self.request.tenant_id)
|
||||
|
||||
def get_serializer_class(self):
|
||||
return super().get_serializer_class()
|
||||
|
||||
|
||||
@extend_schema_view(
|
||||
list=extend_schema(
|
||||
tags=["Processor"],
|
||||
|
||||
@@ -1,163 +0,0 @@
|
||||
from typing import Dict, Set
|
||||
|
||||
import openai
|
||||
from celery.utils.log import get_task_logger
|
||||
|
||||
from api.models import LighthouseProviderConfiguration, LighthouseProviderModels
|
||||
|
||||
logger = get_task_logger(__name__)
|
||||
|
||||
|
||||
def _extract_openai_api_key(
|
||||
provider_cfg: LighthouseProviderConfiguration,
|
||||
) -> str | None:
|
||||
"""
|
||||
Safely extract the OpenAI API key from a provider configuration.
|
||||
|
||||
Args:
|
||||
provider_cfg (LighthouseProviderConfiguration): The provider configuration instance
|
||||
containing the credentials.
|
||||
|
||||
Returns:
|
||||
str | None: The API key string if present and valid, otherwise None.
|
||||
"""
|
||||
creds = provider_cfg.credentials_decoded
|
||||
if not isinstance(creds, dict):
|
||||
return None
|
||||
api_key = creds.get("api_key")
|
||||
if not isinstance(api_key, str) or not api_key:
|
||||
return None
|
||||
return api_key
|
||||
|
||||
|
||||
def check_lighthouse_provider_connection(provider_config_id: str) -> Dict:
|
||||
"""
|
||||
Validate a Lighthouse provider configuration by calling the provider API and
|
||||
toggle its active state accordingly.
|
||||
|
||||
Currently supports the OpenAI provider by invoking `models.list` to verify that
|
||||
the provided credentials are valid.
|
||||
|
||||
Args:
|
||||
provider_config_id (str): The primary key of the `LighthouseProviderConfiguration`
|
||||
to validate.
|
||||
|
||||
Returns:
|
||||
dict: A result dictionary with the following keys:
|
||||
- "connected" (bool): Whether the provider credentials are valid.
|
||||
- "error" (str | None): The error message when not connected, otherwise None.
|
||||
|
||||
Side Effects:
|
||||
- Updates and persists `is_active` on the `LighthouseProviderConfiguration`.
|
||||
|
||||
Raises:
|
||||
LighthouseProviderConfiguration.DoesNotExist: If no configuration exists with the given ID.
|
||||
"""
|
||||
provider_cfg = LighthouseProviderConfiguration.objects.get(pk=provider_config_id)
|
||||
|
||||
# TODO: Add support for other providers
|
||||
if (
|
||||
provider_cfg.provider_type
|
||||
!= LighthouseProviderConfiguration.LLMProviderChoices.OPENAI
|
||||
):
|
||||
return {"connected": False, "error": "Unsupported provider type"}
|
||||
|
||||
api_key = _extract_openai_api_key(provider_cfg)
|
||||
if not api_key:
|
||||
provider_cfg.is_active = False
|
||||
provider_cfg.save()
|
||||
return {"connected": False, "error": "API key is invalid or missing"}
|
||||
|
||||
try:
|
||||
client = openai.OpenAI(api_key=api_key)
|
||||
_ = client.models.list()
|
||||
provider_cfg.is_active = True
|
||||
provider_cfg.save()
|
||||
return {"connected": True, "error": None}
|
||||
except Exception as e:
|
||||
logger.warning("OpenAI connection check failed: %s", str(e))
|
||||
provider_cfg.is_active = False
|
||||
provider_cfg.save()
|
||||
return {"connected": False, "error": str(e)}
|
||||
|
||||
|
||||
def refresh_lighthouse_provider_models(provider_config_id: str) -> Dict:
|
||||
"""
|
||||
Refresh the catalog of models for a Lighthouse provider configuration.
|
||||
|
||||
For the OpenAI provider, this fetches the current list of models, upserts entries
|
||||
into `LighthouseProviderModels`, and deletes stale entries no longer returned by
|
||||
the provider.
|
||||
|
||||
Args:
|
||||
provider_config_id (str): The primary key of the `LighthouseProviderConfiguration`
|
||||
whose models should be refreshed.
|
||||
|
||||
Returns:
|
||||
dict: A result dictionary with the following keys on success:
|
||||
- "created" (int): Number of new model rows created.
|
||||
- "updated" (int): Number of existing model rows updated.
|
||||
- "deleted" (int): Number of stale model rows removed.
|
||||
If an error occurs, the dictionary will contain an "error" (str) field instead.
|
||||
|
||||
Raises:
|
||||
LighthouseProviderConfiguration.DoesNotExist: If no configuration exists with the given ID.
|
||||
"""
|
||||
provider_cfg = LighthouseProviderConfiguration.objects.get(pk=provider_config_id)
|
||||
|
||||
if (
|
||||
provider_cfg.provider_type
|
||||
!= LighthouseProviderConfiguration.LLMProviderChoices.OPENAI
|
||||
):
|
||||
return {
|
||||
"created": 0,
|
||||
"updated": 0,
|
||||
"deleted": 0,
|
||||
"error": "Unsupported provider type",
|
||||
}
|
||||
|
||||
api_key = _extract_openai_api_key(provider_cfg)
|
||||
if not api_key:
|
||||
return {
|
||||
"created": 0,
|
||||
"updated": 0,
|
||||
"deleted": 0,
|
||||
"error": "API key is invalid or missing",
|
||||
}
|
||||
|
||||
try:
|
||||
client = openai.OpenAI(api_key=api_key)
|
||||
models = client.models.list()
|
||||
fetched_ids: Set[str] = {m.id for m in getattr(models, "data", [])}
|
||||
except Exception as e: # noqa: BLE001
|
||||
logger.warning("OpenAI models refresh failed: %s", str(e))
|
||||
return {"created": 0, "updated": 0, "deleted": 0, "error": str(e)}
|
||||
|
||||
created = 0
|
||||
updated = 0
|
||||
|
||||
for model_id in fetched_ids:
|
||||
obj, was_created = LighthouseProviderModels.objects.update_or_create(
|
||||
tenant_id=provider_cfg.tenant_id,
|
||||
provider_configuration=provider_cfg,
|
||||
model_id=model_id,
|
||||
defaults={
|
||||
"model_name": model_id, # OpenAI doesn't return a separate display name
|
||||
"default_parameters": {},
|
||||
},
|
||||
)
|
||||
if was_created:
|
||||
created += 1
|
||||
else:
|
||||
updated += 1
|
||||
|
||||
# Delete stale models not present anymore
|
||||
deleted, _ = (
|
||||
LighthouseProviderModels.objects.filter(
|
||||
tenant_id=provider_cfg.tenant_id, provider_configuration=provider_cfg
|
||||
)
|
||||
.exclude(model_id__in=fetched_ids)
|
||||
.delete()
|
||||
)
|
||||
|
||||
return {"created": created, "updated": updated, "deleted": deleted}
|
||||
@@ -27,10 +27,6 @@ from tasks.jobs.integrations import (
|
||||
upload_s3_integration,
|
||||
upload_security_hub_integration,
|
||||
)
|
||||
from tasks.jobs.lighthouse_providers import (
|
||||
check_lighthouse_provider_connection,
|
||||
refresh_lighthouse_provider_models,
|
||||
)
|
||||
from tasks.jobs.report import generate_threatscore_report_job
|
||||
from tasks.jobs.scan import (
|
||||
aggregate_findings,
|
||||
@@ -528,24 +524,6 @@ def check_lighthouse_connection_task(lighthouse_config_id: str, tenant_id: str =
|
||||
return check_lighthouse_connection(lighthouse_config_id=lighthouse_config_id)
|
||||
|
||||
|
||||
@shared_task(base=RLSTask, name="lighthouse-provider-connection-check")
|
||||
@set_tenant
|
||||
def check_lighthouse_provider_connection_task(
|
||||
provider_config_id: str, tenant_id: str | None = None
|
||||
) -> dict:
|
||||
"""Task wrapper to validate provider credentials and set is_active."""
|
||||
return check_lighthouse_provider_connection(provider_config_id=provider_config_id)
|
||||
|
||||
|
||||
@shared_task(base=RLSTask, name="lighthouse-provider-models-refresh")
|
||||
@set_tenant
|
||||
def refresh_lighthouse_provider_models_task(
|
||||
provider_config_id: str, tenant_id: str | None = None
|
||||
) -> dict:
|
||||
"""Task wrapper to refresh provider models catalog for the given configuration."""
|
||||
return refresh_lighthouse_provider_models(provider_config_id=provider_config_id)
|
||||
|
||||
|
||||
@shared_task(name="integration-check")
|
||||
def check_integrations_task(tenant_id: str, provider_id: str, scan_id: str = None):
|
||||
"""
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
"""
|
||||
CIS Alibaba Cloud Compliance Dashboard
|
||||
|
||||
This module generates compliance reports for the CIS Alibaba Cloud Foundations Benchmark.
|
||||
"""
|
||||
|
||||
import warnings
|
||||
|
||||
from dashboard.common_methods import get_section_containers_3_levels
|
||||
|
||||
warnings.filterwarnings("ignore")
|
||||
|
||||
|
||||
def get_table(data):
|
||||
"""
|
||||
Generate compliance table for CIS Alibaba Cloud framework
|
||||
|
||||
This function processes compliance data and formats it for dashboard display,
|
||||
with sections, subsections, and individual requirements.
|
||||
|
||||
Args:
|
||||
data: DataFrame containing compliance data with columns:
|
||||
- REQUIREMENTS_ID: Requirement identifier (e.g., "2.1", "4.1")
|
||||
- REQUIREMENTS_DESCRIPTION: Description of the requirement
|
||||
- REQUIREMENTS_ATTRIBUTES_SECTION: Main section (e.g., "2. Storage")
|
||||
- REQUIREMENTS_ATTRIBUTES_SUBSECTION: Subsection (e.g., "2.1 ECS Disk Encryption")
|
||||
- CHECKID: Associated Prowler check ID
|
||||
- STATUS: Check status (PASS/FAIL)
|
||||
- REGION: Alibaba Cloud region
|
||||
- ACCOUNTID: Alibaba Cloud account ID
|
||||
- RESOURCEID: Resource identifier
|
||||
|
||||
Returns:
|
||||
Dashboard table with hierarchical compliance structure
|
||||
"""
|
||||
# Format requirement descriptions with ID prefix and truncate if too long
|
||||
data["REQUIREMENTS_DESCRIPTION"] = (
|
||||
data["REQUIREMENTS_ID"] + " - " + data["REQUIREMENTS_DESCRIPTION"]
|
||||
)
|
||||
|
||||
data["REQUIREMENTS_DESCRIPTION"] = data["REQUIREMENTS_DESCRIPTION"].apply(
|
||||
lambda x: x[:150] + "..." if len(str(x)) > 150 else x
|
||||
)
|
||||
|
||||
# Truncate section names if too long
|
||||
data["REQUIREMENTS_ATTRIBUTES_SECTION"] = data[
|
||||
"REQUIREMENTS_ATTRIBUTES_SECTION"
|
||||
].apply(lambda x: x[:80] + "..." if len(str(x)) > 80 else x)
|
||||
|
||||
# Truncate subsection names if too long
|
||||
data["REQUIREMENTS_ATTRIBUTES_SUBSECTION"] = data[
|
||||
"REQUIREMENTS_ATTRIBUTES_SUBSECTION"
|
||||
].apply(lambda x: x[:150] + "..." if len(str(x)) > 150 else x)
|
||||
|
||||
# Select relevant columns for display
|
||||
display_data = data[
|
||||
[
|
||||
"REQUIREMENTS_DESCRIPTION",
|
||||
"REQUIREMENTS_ATTRIBUTES_SECTION",
|
||||
"REQUIREMENTS_ATTRIBUTES_SUBSECTION",
|
||||
"CHECKID",
|
||||
"STATUS",
|
||||
"REGION",
|
||||
"ACCOUNTID",
|
||||
"RESOURCEID",
|
||||
]
|
||||
]
|
||||
|
||||
# Generate hierarchical table with 3 levels (Section > Subsection > Requirement)
|
||||
return get_section_containers_3_levels(
|
||||
display_data,
|
||||
"REQUIREMENTS_ATTRIBUTES_SECTION",
|
||||
"REQUIREMENTS_ATTRIBUTES_SUBSECTION",
|
||||
"REQUIREMENTS_DESCRIPTION",
|
||||
)
|
||||
+1
-20
@@ -98,7 +98,7 @@
|
||||
]
|
||||
},
|
||||
"user-guide/tutorials/prowler-app-rbac",
|
||||
"user-guide/tutorials/prowler-app-api-keys",
|
||||
"user-guide/providers/prowler-app-api-keys",
|
||||
"user-guide/tutorials/prowler-app-mute-findings",
|
||||
{
|
||||
"group": "Integrations",
|
||||
@@ -251,25 +251,6 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"tab": "Workshop",
|
||||
"groups": [
|
||||
{
|
||||
"group": "Hands-On Labs",
|
||||
"pages": [
|
||||
"workshop/introduction",
|
||||
"workshop/lab-01-getting-started",
|
||||
"workshop/lab-02-threat-detection",
|
||||
"workshop/lab-03-custom-checks",
|
||||
"workshop/lab-04-azure-multicloud",
|
||||
"workshop/lab-05-gcp-multicloud",
|
||||
"workshop/lab-06-compliance-as-code",
|
||||
"workshop/lab-07-integrations",
|
||||
"workshop/lab-08-prowler-saas"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"tab": "Developer Guide",
|
||||
"groups": [
|
||||
|
||||
@@ -10,7 +10,7 @@ Configure your MCP client to connect to Prowler MCP Server.
|
||||
**Authentication is optional**: Prowler Hub and Prowler Documentation features work without authentication. An API key is only required for Prowler Cloud and Prowler App (Self-Managed) features.
|
||||
</Note>
|
||||
|
||||
To use Prowler Cloud or Prowler App (Self-Managed) features. To get the API key, please refer to the [API Keys](/user-guide/tutorials/prowler-app-api-keys) guide.
|
||||
To use Prowler Cloud or Prowler App (Self-Managed) features. To get the API key, please refer to the [API Keys](/user-guide/providers/prowler-app-api-keys) guide.
|
||||
|
||||
<Warning>
|
||||
Keep the API key secure. Never share it publicly or commit it to version control.
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
export const VersionBadge = ({ version }) => {
|
||||
return (
|
||||
<code className="version-badge-container">
|
||||
<p className="version-badge">
|
||||
<span className="version-badge-label">Added in:</span>
|
||||
<code className="version-badge-version">{version}</code>
|
||||
</p>
|
||||
</code>
|
||||
|
||||
|
||||
);
|
||||
};
|
||||
@@ -1,51 +0,0 @@
|
||||
/* Version Badge Styling */
|
||||
.version-badge-container {
|
||||
display: inline-block;
|
||||
margin: 0 0 1rem 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.version-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
margin: 0;
|
||||
padding: 0.375rem 0.75rem;
|
||||
background: linear-gradient(135deg, #1a1a1a 0%, #000000 100%);
|
||||
color: #ffffff;
|
||||
border-radius: 1.25rem;
|
||||
font-weight: 400;
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.25rem;
|
||||
border: 1px solid rgba(0, 0, 0, 0.15);
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.version-badge-label {
|
||||
font-weight: 400;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.version-badge-version {
|
||||
background: rgba(255, 255, 255, 0.12);
|
||||
padding: 0.125rem 0.5rem;
|
||||
border-radius: 0.875rem;
|
||||
font-family: ui-monospace, SFMono-Regular, 'SF Mono', Menlo, Monaco, 'Cascadia Code', 'Roboto Mono', Consolas, 'Courier New', monospace;
|
||||
font-weight: 600;
|
||||
font-size: 0.875rem;
|
||||
color: #ffffff;
|
||||
border: none;
|
||||
}
|
||||
|
||||
|
||||
.dark .version-badge {
|
||||
background: #55B685;
|
||||
color: #000000;
|
||||
border: 2px solid rgba(85, 182, 133, 0.3);
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.dark .version-badge-version {
|
||||
background: rgba(0, 0, 0, 0.1);
|
||||
color: #000000;
|
||||
border: none;
|
||||
}
|
||||
-4
@@ -2,10 +2,6 @@
|
||||
title: 'API Keys'
|
||||
---
|
||||
|
||||
import { VersionBadge } from "/snippets/version-badge.mdx"
|
||||
|
||||
<VersionBadge version="5.13.0" />
|
||||
|
||||
API key authentication in Prowler App provides an alternative to JWT tokens and empowers automation, CI/CD pipelines, and third-party integrations. This guide explains how to create, manage, and safeguard API keys when working with the Prowler API.
|
||||
|
||||
## API Key Advantages
|
||||
@@ -32,7 +32,7 @@ The AWS Organizations Bulk Provisioning tool simplifies multi-account onboarding
|
||||
* ProwlerRole (or custom role) deployed across all target accounts
|
||||
* Prowler API key (from Prowler Cloud or self-hosted Prowler App)
|
||||
* For self-hosted Prowler App, remember to [point to your API base URL](./bulk-provider-provisioning#custom-api-endpoints)
|
||||
* Learn how to create API keys: [Prowler App API Keys](../tutorials/prowler-app-api-keys)
|
||||
* Learn how to create API keys: [Prowler App API Keys](../providers/prowler-app-api-keys)
|
||||
|
||||
### Deploying ProwlerRole Across AWS Organizations
|
||||
|
||||
@@ -101,7 +101,7 @@ To create an API key:
|
||||
4. Provide a descriptive name and optionally set an expiration date
|
||||
5. Copy the generated API key (it will only be shown once)
|
||||
|
||||
For detailed instructions, see: [Prowler App API Keys](../tutorials/prowler-app-api-keys)
|
||||
For detailed instructions, see: [Prowler App API Keys](../providers/prowler-app-api-keys)
|
||||
|
||||
## Basic Usage
|
||||
|
||||
|
||||
@@ -28,7 +28,7 @@ The Bulk Provider Provisioning tool automates the creation of cloud providers in
|
||||
* Python 3.7 or higher
|
||||
* Prowler API key (from Prowler Cloud or self-hosted Prowler App)
|
||||
* For self-hosted Prowler App, remember to [point to your API base URL](#custom-api-endpoints)
|
||||
* Learn how to create API keys: [Prowler App API Keys](../tutorials/prowler-app-api-keys)
|
||||
* Learn how to create API keys: [Prowler App API Keys](../providers/prowler-app-api-keys)
|
||||
* Authentication credentials for target cloud providers
|
||||
|
||||
### Installation
|
||||
@@ -57,7 +57,7 @@ To create an API key:
|
||||
4. Provide a descriptive name and optionally set an expiration date
|
||||
5. Copy the generated API key (it will only be shown once)
|
||||
|
||||
For detailed instructions, see: [Prowler App API Keys](../tutorials/prowler-app-api-keys)
|
||||
For detailed instructions, see: [Prowler App API Keys](../providers/prowler-app-api-keys)
|
||||
|
||||
## Configuration File Structure
|
||||
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
---
|
||||
title: "Jira Integration"
|
||||
---
|
||||
import { VersionBadge } from "/snippets/version-badge.mdx"
|
||||
|
||||
<VersionBadge version="5.12.0" />
|
||||
|
||||
Prowler App enables automatic export of security findings to Jira, providing seamless integration with Atlassian's work item tracking and project management platform. This comprehensive guide demonstrates how to configure and manage Jira integrations to streamline security incident management and enhance team collaboration across security workflows.
|
||||
|
||||
|
||||
@@ -2,10 +2,6 @@
|
||||
title: 'Prowler Lighthouse AI'
|
||||
---
|
||||
|
||||
import { VersionBadge } from "/snippets/version-badge.mdx"
|
||||
|
||||
<VersionBadge version="5.8.0" />
|
||||
|
||||
Prowler Lighthouse AI is a Cloud Security Analyst chatbot that helps you understand, prioritize, and remediate security findings in your cloud environments. It's designed to provide security expertise for teams without dedicated resources, acting as your 24/7 virtual cloud security analyst.
|
||||
|
||||
<img src="/images/prowler-app/lighthouse-intro.png" alt="Prowler Lighthouse" />
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
---
|
||||
title: 'Mute Findings (Mutelist)'
|
||||
---
|
||||
import { VersionBadge } from "/snippets/version-badge.mdx"
|
||||
|
||||
<VersionBadge version="5.9.0" />
|
||||
|
||||
Prowler App allows users to mute specific findings to focus on the most critical security issues. This comprehensive guide demonstrates how to effectively use the Mutelist feature to manage and prioritize security findings.
|
||||
|
||||
|
||||
@@ -2,10 +2,6 @@
|
||||
title: 'Managing Users and Role-Based Access Control (RBAC)'
|
||||
---
|
||||
|
||||
import { VersionBadge } from "/snippets/version-badge.mdx"
|
||||
|
||||
<VersionBadge version="5.1.0" />
|
||||
|
||||
**Prowler App** supports multiple users within a single tenant, enabling seamless collaboration by allowing team members to easily share insights and manage security findings.
|
||||
|
||||
[Roles](#roles) help you control user permissions, determining what actions each user can perform and the data they can access within Prowler. By default, each account includes an immutable **admin** role, ensuring that your account always retains administrative access.
|
||||
|
||||
@@ -2,10 +2,6 @@
|
||||
title: 'Amazon S3 Integration'
|
||||
---
|
||||
|
||||
import { VersionBadge } from "/snippets/version-badge.mdx"
|
||||
|
||||
<VersionBadge version="5.10.0" />
|
||||
|
||||
**Prowler App** allows automatic export of scan results to Amazon S3 buckets, providing seamless integration with existing data workflows and storage infrastructure. This comprehensive guide demonstrates configuration and management of Amazon S3 integrations to streamline security finding management and reporting.
|
||||
|
||||
When enabled and configured, scan results are automatically stored in the configured bucket. Results are provided in `csv`, `html` and `json-ocsf` formats, offering flexibility for custom integrations:
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
---
|
||||
title: "AWS Security Hub Integration"
|
||||
---
|
||||
import { VersionBadge } from "/snippets/version-badge.mdx"
|
||||
|
||||
<VersionBadge version="5.11.0" />
|
||||
|
||||
Prowler App enables automatic export of security findings to AWS Security Hub, providing seamless integration with AWS's native security and compliance service. This comprehensive guide demonstrates how to configure and manage AWS Security Hub integrations to centralize security findings and enhance compliance tracking across AWS environments.
|
||||
|
||||
|
||||
@@ -2,10 +2,6 @@
|
||||
title: 'Social Login Configuration'
|
||||
---
|
||||
|
||||
import { VersionBadge } from "/snippets/version-badge.mdx"
|
||||
|
||||
<VersionBadge version="5.5.0" />
|
||||
|
||||
**Prowler App** supports social login using Google and GitHub OAuth providers. This document guides you through configuring the required environment variables to enable social authentication.
|
||||
|
||||
<img src="/images/prowler-app/social-login/social_login_buttons.png" alt="Social login buttons" width="700" />
|
||||
|
||||
@@ -2,10 +2,6 @@
|
||||
title: 'SAML Single Sign-On (SSO)'
|
||||
---
|
||||
|
||||
import { VersionBadge } from "/snippets/version-badge.mdx"
|
||||
|
||||
<VersionBadge version="5.9.0" />
|
||||
|
||||
This guide provides comprehensive instructions to configure SAML-based Single Sign-On (SSO) in Prowler App. This configuration allows users to authenticate using the organization's Identity Provider (IdP).
|
||||
|
||||
This document is divided into two main sections:
|
||||
|
||||
@@ -1,54 +0,0 @@
|
||||
---
|
||||
title: "Workshop Introduction"
|
||||
description: "Hands-on labs to master Prowler's cloud security capabilities across AWS, Azure, and GCP"
|
||||
---
|
||||
|
||||
# Prowler Workshop
|
||||
|
||||
Welcome to the Prowler Workshop. This hands-on training provides practical experience with Prowler's cloud security monitoring and compliance automation capabilities across multiple cloud platforms.
|
||||
|
||||
## Workshop Overview
|
||||
|
||||
This workshop consists of eight progressive labs designed to guide you through Prowler's core features and advanced capabilities:
|
||||
|
||||
* **Lab 1:** Getting Started with Prowler CLI
|
||||
* **Lab 2:** Threat Detection with Prowler
|
||||
* **Lab 3:** Custom Checks with Prowler
|
||||
* **Lab 4:** Multi-Cloud Security with Prowler (Azure)
|
||||
* **Lab 5:** Multi-Cloud Security with Prowler (GCP)
|
||||
* **Lab 6:** Compliance as Code with Prowler
|
||||
* **Lab 7:** Integrations with Prowler (AWS Security Hub)
|
||||
* **Lab 8:** Prowler SaaS Platform
|
||||
|
||||
## Lab Structure
|
||||
|
||||
Each lab is self-contained and includes:
|
||||
|
||||
* **Prerequisites:** Required cloud accounts, tools, and prior lab dependencies
|
||||
* **Objectives:** Clear learning goals for the lab
|
||||
* **Step-by-step instructions:** Detailed guidance through each task
|
||||
* **Expected outcomes:** What you should achieve by completing the lab
|
||||
* **Verification steps:** How to confirm successful completion
|
||||
|
||||
## Prerequisites Approach
|
||||
|
||||
Each lab specifies its own prerequisites, as different labs require different cloud provider accounts, tools, and access levels. Review the prerequisites section at the beginning of each lab before starting.
|
||||
|
||||
## How to Use This Workshop
|
||||
|
||||
* Labs are designed to be completed sequentially, as later labs may build on concepts from earlier ones
|
||||
* Estimated time to complete varies by lab (typically 30-60 minutes each)
|
||||
* You can pause between labs and resume later
|
||||
* Some labs can be completed independently if you have the necessary prerequisites
|
||||
|
||||
## Getting Help
|
||||
|
||||
If you encounter issues during the workshop:
|
||||
|
||||
* Refer to the [Troubleshooting](/troubleshooting) guide
|
||||
* Join the [Prowler Slack community](https://goto.prowler.com/slack)
|
||||
* Visit the [Prowler GitHub repository](https://github.com/prowler-cloud/prowler) for documentation and issues
|
||||
|
||||
## Ready to Start?
|
||||
|
||||
Begin with [Lab 1: Getting Started with Prowler CLI](/workshop/lab-01-getting-started) to set up your environment and run your first security scan.
|
||||
@@ -1,203 +0,0 @@
|
||||
---
|
||||
title: "Lab 1: Getting Started with Prowler CLI"
|
||||
description: "Install Prowler CLI and run your first cloud security assessment on AWS"
|
||||
---
|
||||
|
||||
<Note>
|
||||
**Tags:** `workshop` `aws` `getting-started` `beginner` `cli`
|
||||
</Note>
|
||||
|
||||
# Lab 1: Getting Started with Prowler CLI
|
||||
|
||||
Learn to install Prowler CLI and perform your first cloud security assessment on AWS.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
* AWS account with active resources
|
||||
* AWS CLI installed and configured
|
||||
* IAM credentials with appropriate permissions (see [AWS Authentication](/user-guide/providers/aws/authentication))
|
||||
* Python 3.9 or higher
|
||||
* Basic command-line experience
|
||||
|
||||
**Estimated Time:** 30 minutes
|
||||
|
||||
## Lab Objectives
|
||||
|
||||
By completing this lab, you will:
|
||||
|
||||
* Install Prowler CLI using pip
|
||||
* Configure AWS credentials for Prowler
|
||||
* Execute your first security scan
|
||||
* Understand Prowler's output formats
|
||||
* Review security findings
|
||||
|
||||
## Step 1: Install Prowler CLI
|
||||
|
||||
Install Prowler using pip:
|
||||
|
||||
```bash
|
||||
pip install prowler
|
||||
```
|
||||
|
||||
Verify the installation:
|
||||
|
||||
```bash
|
||||
prowler -v
|
||||
```
|
||||
|
||||
Expected output:
|
||||
```
|
||||
Prowler X.X.X
|
||||
```
|
||||
|
||||
<Tip>
|
||||
For alternative installation methods (Docker, from source), see [Prowler CLI Installation](/getting-started/installation/prowler-cli).
|
||||
</Tip>
|
||||
|
||||
## Step 2: Configure AWS Credentials
|
||||
|
||||
Ensure AWS credentials are configured. Prowler uses the same credential chain as AWS CLI.
|
||||
|
||||
Verify credentials:
|
||||
|
||||
```bash
|
||||
aws sts get-caller-identity
|
||||
```
|
||||
|
||||
Expected output:
|
||||
```json
|
||||
{
|
||||
"UserId": "AIDACKCEVSQ6C2EXAMPLE",
|
||||
"Account": "123456789012",
|
||||
"Arn": "arn:aws:iam::123456789012:user/username"
|
||||
}
|
||||
```
|
||||
|
||||
<Note>
|
||||
[Note: Screenshot of slide 8 showing AWS credential verification - to be added]
|
||||
</Note>
|
||||
|
||||
## Step 3: Run Your First Scan
|
||||
|
||||
Execute a basic Prowler scan:
|
||||
|
||||
```bash
|
||||
prowler aws
|
||||
```
|
||||
|
||||
This command:
|
||||
* Scans all enabled AWS regions
|
||||
* Runs all available security checks
|
||||
* Generates output in the current directory
|
||||
|
||||
<Note>
|
||||
The scan may take 5-15 minutes depending on the number of resources in your AWS account.
|
||||
</Note>
|
||||
|
||||
## Step 4: Understanding the Output
|
||||
|
||||
Prowler generates multiple output formats in the `output` directory:
|
||||
|
||||
* **CSV:** Detailed findings (`prowler-output-*.csv`)
|
||||
* **JSON:** Machine-readable format (`prowler-output-*.json`)
|
||||
* **HTML:** Human-readable report (`prowler-output-*.html`)
|
||||
|
||||
Review the HTML report:
|
||||
|
||||
```bash
|
||||
open output/prowler-output-*.html
|
||||
```
|
||||
|
||||
<Note>
|
||||
[Note: Screenshot of slide 10 showing HTML report - to be added]
|
||||
</Note>
|
||||
|
||||
## Step 5: Analyze Security Findings
|
||||
|
||||
Examine the findings structure in the HTML report:
|
||||
|
||||
* **Status:** PASS, FAIL, or MANUAL
|
||||
* **Severity:** critical, high, medium, low, informational
|
||||
* **Service:** AWS service affected (e.g., S3, IAM, EC2)
|
||||
* **Check ID:** Unique identifier for each check
|
||||
* **Region:** AWS region where the resource exists
|
||||
* **Resource:** Specific resource ARN or identifier
|
||||
|
||||
Example finding structure:
|
||||
|
||||
```json
|
||||
{
|
||||
"Status": "FAIL",
|
||||
"Severity": "high",
|
||||
"Service": "s3",
|
||||
"CheckID": "s3_bucket_public_access",
|
||||
"Region": "us-east-1",
|
||||
"Resource": "arn:aws:s3:::my-bucket"
|
||||
}
|
||||
```
|
||||
|
||||
## Step 6: Filter Scan by Service
|
||||
|
||||
Run a targeted scan for specific AWS services:
|
||||
|
||||
```bash
|
||||
prowler aws --services s3 iam
|
||||
```
|
||||
|
||||
This scans only S3 and IAM services, reducing execution time.
|
||||
|
||||
## Step 7: Run Checks by Severity
|
||||
|
||||
Scan for critical and high-severity findings only:
|
||||
|
||||
```bash
|
||||
prowler aws --severity critical high
|
||||
```
|
||||
|
||||
This focuses on the most important security issues.
|
||||
|
||||
<Note>
|
||||
[Note: Screenshot of slide 13 showing severity filtering - to be added]
|
||||
</Note>
|
||||
|
||||
## Verification Steps
|
||||
|
||||
Confirm successful lab completion:
|
||||
|
||||
1. Prowler CLI installed and version verified
|
||||
2. AWS credentials properly configured
|
||||
3. First scan completed successfully
|
||||
4. Output files generated in the `output` directory
|
||||
5. HTML report reviewed and findings understood
|
||||
6. Filtered scans executed by service and severity
|
||||
|
||||
## Expected Outcomes
|
||||
|
||||
After completing this lab, you should have:
|
||||
|
||||
* Working Prowler CLI installation
|
||||
* Understanding of basic Prowler commands
|
||||
* Knowledge of output formats
|
||||
* Ability to run targeted scans
|
||||
* Familiarity with finding severity levels
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
**Issue:** `prowler: command not found`
|
||||
* **Solution:** Ensure Python's bin directory is in your PATH, or use `python3 -m prowler`
|
||||
|
||||
**Issue:** AWS credentials error
|
||||
* **Solution:** Run `aws configure` to set up credentials, or use environment variables
|
||||
|
||||
**Issue:** Scan takes too long
|
||||
* **Solution:** Use `--services` to scan specific services or `--regions` to limit regions
|
||||
|
||||
## Next Steps
|
||||
|
||||
Continue to [Lab 2: Threat Detection with Prowler](/workshop/lab-02-threat-detection) to learn about identifying security threats in your AWS environment.
|
||||
|
||||
## Additional Resources
|
||||
|
||||
* [Prowler CLI Documentation](/getting-started/basic-usage/prowler-cli)
|
||||
* [AWS Authentication Methods](/user-guide/providers/aws/authentication)
|
||||
* [Output Formats](/user-guide/cli/tutorials/reporting)
|
||||
@@ -1,263 +0,0 @@
|
||||
---
|
||||
title: "Lab 2: Threat Detection with Prowler"
|
||||
description: "Identify and analyze security threats in AWS environments using Prowler's threat detection capabilities"
|
||||
---
|
||||
|
||||
<Note>
|
||||
**Tags:** `workshop` `aws` `threat-detection` `intermediate` `security`
|
||||
</Note>
|
||||
|
||||
# Lab 2: Threat Detection with Prowler
|
||||
|
||||
Learn to identify security threats, exposed resources, and potential attack vectors in AWS environments using Prowler's threat detection features.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
* Completion of [Lab 1: Getting Started with Prowler CLI](/workshop/lab-01-getting-started)
|
||||
* AWS account with resources (EC2 instances, S3 buckets, security groups)
|
||||
* Prowler CLI installed and configured
|
||||
* Basic understanding of AWS security concepts
|
||||
|
||||
**Estimated Time:** 45 minutes
|
||||
|
||||
## Lab Objectives
|
||||
|
||||
By completing this lab, you will:
|
||||
|
||||
* Understand Prowler's threat detection capabilities
|
||||
* Identify publicly exposed resources
|
||||
* Detect insecure configurations
|
||||
* Analyze CloudTrail events for suspicious activity
|
||||
* Prioritize security findings by risk
|
||||
|
||||
## Step 1: Understanding Threat Detection Checks
|
||||
|
||||
Prowler includes checks that identify:
|
||||
|
||||
* Public exposure (S3 buckets, EC2 instances, RDS databases)
|
||||
* Insecure network configurations (security groups, NACLs)
|
||||
* Weak encryption settings
|
||||
* Suspicious IAM permissions
|
||||
* CloudTrail anomalies
|
||||
|
||||
List threat detection checks:
|
||||
|
||||
```bash
|
||||
prowler aws --list-checks | grep -i "public\|exposed\|open"
|
||||
```
|
||||
|
||||
## Step 2: Scan for Publicly Exposed Resources
|
||||
|
||||
Run a scan focusing on public exposure:
|
||||
|
||||
```bash
|
||||
prowler aws --checks s3_bucket_public_access ec2_instance_public_ip rds_instance_publicly_accessible
|
||||
```
|
||||
|
||||
This identifies:
|
||||
* S3 buckets with public access
|
||||
* EC2 instances with public IPs
|
||||
* RDS databases accessible from the internet
|
||||
|
||||
<Note>
|
||||
[Note: Screenshot of slide 17 showing public exposure findings - to be added]
|
||||
</Note>
|
||||
|
||||
## Step 3: Analyze Security Group Misconfigurations
|
||||
|
||||
Security groups control network access. Scan for insecure rules:
|
||||
|
||||
```bash
|
||||
prowler aws --services ec2 --checks ec2_securitygroup*
|
||||
```
|
||||
|
||||
Look for findings related to:
|
||||
* `0.0.0.0/0` ingress rules (any IP can connect)
|
||||
* Open high-risk ports (22, 3389, 3306, 5432)
|
||||
* Overly permissive egress rules
|
||||
|
||||
Example vulnerable security group:
|
||||
```
|
||||
Port 22 (SSH) open to 0.0.0.0/0
|
||||
Port 3389 (RDP) open to 0.0.0.0/0
|
||||
```
|
||||
|
||||
<Warning>
|
||||
Security groups with `0.0.0.0/0` on sensitive ports expose resources to the entire internet and should be restricted immediately.
|
||||
</Warning>
|
||||
|
||||
## Step 4: Check for Unencrypted Data
|
||||
|
||||
Scan for unencrypted storage and data transmission:
|
||||
|
||||
```bash
|
||||
prowler aws --checks s3_bucket_default_encryption ebs_volume_encryption rds_instance_storage_encrypted
|
||||
```
|
||||
|
||||
Key checks:
|
||||
* S3 bucket default encryption disabled
|
||||
* EBS volumes without encryption
|
||||
* RDS instances with unencrypted storage
|
||||
|
||||
<Note>
|
||||
[Note: Screenshot of slide 20 showing encryption findings - to be added]
|
||||
</Note>
|
||||
|
||||
## Step 5: CloudTrail Threat Detection
|
||||
|
||||
Enable CloudTrail event analysis to detect suspicious activity:
|
||||
|
||||
```bash
|
||||
prowler aws --services cloudtrail
|
||||
```
|
||||
|
||||
Prowler checks for:
|
||||
* CloudTrail disabled in regions
|
||||
* Log file validation disabled
|
||||
* S3 bucket not encrypted
|
||||
* CloudWatch logging not configured
|
||||
|
||||
<Tip>
|
||||
CloudTrail provides audit logs of API calls. Proper configuration is essential for threat detection and incident response.
|
||||
</Tip>
|
||||
|
||||
## Step 6: Analyze IAM Security Risks
|
||||
|
||||
Identify IAM misconfigurations that could lead to privilege escalation:
|
||||
|
||||
```bash
|
||||
prowler aws --services iam --severity critical high
|
||||
```
|
||||
|
||||
Look for:
|
||||
* Root account usage
|
||||
* IAM users without MFA
|
||||
* Overly permissive IAM policies (e.g., `*:*`)
|
||||
* Inactive credentials not rotated
|
||||
|
||||
Example critical finding:
|
||||
```
|
||||
IAM user with administrative privileges without MFA enabled
|
||||
```
|
||||
|
||||
## Step 7: Generate a Threat-Focused Report
|
||||
|
||||
Create a filtered report with only security threats:
|
||||
|
||||
```bash
|
||||
prowler aws --severity critical high --status FAIL -o html json
|
||||
```
|
||||
|
||||
This generates reports containing only:
|
||||
* Critical and high-severity findings
|
||||
* Failed checks (PASS checks excluded)
|
||||
|
||||
Review the HTML report:
|
||||
|
||||
```bash
|
||||
open output/prowler-output-*.html
|
||||
```
|
||||
|
||||
<Note>
|
||||
[Note: Screenshot of slide 25 showing threat-focused report - to be added]
|
||||
</Note>
|
||||
|
||||
## Step 8: Prioritize Findings
|
||||
|
||||
Categorize findings by risk level:
|
||||
|
||||
**Critical Priority (Address Immediately):**
|
||||
* S3 buckets with public write access
|
||||
* Root account without MFA
|
||||
* Database instances publicly accessible
|
||||
* Security groups open to `0.0.0.0/0` on sensitive ports
|
||||
|
||||
**High Priority (Address Soon):**
|
||||
* Unencrypted storage volumes
|
||||
* CloudTrail logging disabled
|
||||
* IAM users without MFA
|
||||
* Overly permissive IAM policies
|
||||
|
||||
**Medium Priority (Address as Resources Allow):**
|
||||
* Old access keys not rotated
|
||||
* S3 bucket logging disabled
|
||||
* VPC flow logs not enabled
|
||||
|
||||
## Step 9: Export Findings for Remediation
|
||||
|
||||
Export findings to CSV for tracking:
|
||||
|
||||
```bash
|
||||
prowler aws --severity critical high --status FAIL -o csv
|
||||
```
|
||||
|
||||
Share the CSV with your security team for remediation tracking.
|
||||
|
||||
## Verification Steps
|
||||
|
||||
Confirm successful lab completion:
|
||||
|
||||
1. Identified publicly exposed resources
|
||||
2. Detected insecure security group configurations
|
||||
3. Found unencrypted data storage
|
||||
4. Reviewed CloudTrail security settings
|
||||
5. Analyzed IAM security risks
|
||||
6. Generated threat-focused reports
|
||||
7. Prioritized findings by risk level
|
||||
|
||||
## Expected Outcomes
|
||||
|
||||
After completing this lab, you should:
|
||||
|
||||
* Understand common AWS security threats
|
||||
* Know how to identify exposed resources
|
||||
* Be able to prioritize security findings
|
||||
* Have generated threat detection reports
|
||||
|
||||
## Remediation Examples
|
||||
|
||||
**Example 1: Remove public access from S3 bucket**
|
||||
```bash
|
||||
aws s3api put-public-access-block \
|
||||
--bucket my-bucket \
|
||||
--public-access-block-configuration \
|
||||
"BlockPublicAcls=true,IgnorePublicAcls=true,BlockPublicPolicy=true,RestrictPublicBuckets=true"
|
||||
```
|
||||
|
||||
**Example 2: Restrict security group rule**
|
||||
```bash
|
||||
aws ec2 revoke-security-group-ingress \
|
||||
--group-id sg-12345678 \
|
||||
--protocol tcp \
|
||||
--port 22 \
|
||||
--cidr 0.0.0.0/0
|
||||
```
|
||||
|
||||
**Example 3: Enable S3 bucket encryption**
|
||||
```bash
|
||||
aws s3api put-bucket-encryption \
|
||||
--bucket my-bucket \
|
||||
--server-side-encryption-configuration \
|
||||
'{"Rules":[{"ApplyServerSideEncryptionByDefault":{"SSEAlgorithm":"AES256"}}]}'
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
**Issue:** Too many findings to review
|
||||
* **Solution:** Use `--severity critical high` to focus on the most important issues first
|
||||
|
||||
**Issue:** Don't understand a finding
|
||||
* **Solution:** Use `--describe-check <check-id>` to get detailed information
|
||||
|
||||
**Issue:** Need to share findings with team
|
||||
* **Solution:** Export to CSV or JSON and use collaboration tools
|
||||
|
||||
## Next Steps
|
||||
|
||||
Continue to [Lab 3: Custom Checks with Prowler](/workshop/lab-03-custom-checks) to learn how to create organization-specific security checks.
|
||||
|
||||
## Additional Resources
|
||||
|
||||
* [AWS Threat Detection Guide](/user-guide/providers/aws/threat-detection)
|
||||
* [Security Best Practices](/user-guide/providers/aws/getting-started-aws)
|
||||
* [Prowler Check Reference](https://hub.prowler.com)
|
||||
@@ -1,359 +0,0 @@
|
||||
---
|
||||
title: "Lab 3: Custom Checks with Prowler"
|
||||
description: "Create organization-specific security checks and customize Prowler for your security requirements"
|
||||
---
|
||||
|
||||
<Note>
|
||||
**Tags:** `workshop` `aws` `custom-checks` `advanced` `development`
|
||||
</Note>
|
||||
|
||||
# Lab 3: Custom Checks with Prowler
|
||||
|
||||
Learn to create custom security checks tailored to your organization's specific security policies and compliance requirements.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
* Completion of [Lab 1: Getting Started with Prowler CLI](/workshop/lab-01-getting-started)
|
||||
* Prowler CLI installed from source (for custom check development)
|
||||
* Python 3.9 or higher
|
||||
* Basic Python programming knowledge
|
||||
* Understanding of AWS SDK (boto3)
|
||||
* Text editor or IDE (VS Code, PyCharm)
|
||||
|
||||
**Estimated Time:** 60 minutes
|
||||
|
||||
## Lab Objectives
|
||||
|
||||
By completing this lab, you will:
|
||||
|
||||
* Understand Prowler's check structure
|
||||
* Create a custom security check
|
||||
* Test and validate custom checks
|
||||
* Use custom check metadata
|
||||
* Integrate custom checks into scans
|
||||
|
||||
## Step 1: Install Prowler from Source
|
||||
|
||||
To develop custom checks, install Prowler from source:
|
||||
|
||||
```bash
|
||||
git clone https://github.com/prowler-cloud/prowler
|
||||
cd prowler
|
||||
pip install poetry
|
||||
poetry install
|
||||
```
|
||||
|
||||
Activate the virtual environment:
|
||||
|
||||
```bash
|
||||
poetry shell
|
||||
```
|
||||
|
||||
Verify installation:
|
||||
|
||||
```bash
|
||||
prowler -v
|
||||
```
|
||||
|
||||
<Note>
|
||||
[Note: Screenshot of slide 29 showing source installation - to be added]
|
||||
</Note>
|
||||
|
||||
## Step 2: Understand Check Structure
|
||||
|
||||
Prowler checks are Python files located in:
|
||||
```
|
||||
prowler/providers/<provider>/services/<service>/
|
||||
```
|
||||
|
||||
Example check structure:
|
||||
```
|
||||
prowler/providers/aws/services/s3/s3_bucket_custom_check/
|
||||
├── s3_bucket_custom_check.py # Check logic
|
||||
└── s3_bucket_custom_check.metadata.json # Check metadata
|
||||
```
|
||||
|
||||
## Step 3: Create a Custom Check Directory
|
||||
|
||||
Create a custom check to verify S3 buckets have specific naming conventions:
|
||||
|
||||
```bash
|
||||
mkdir -p prowler/providers/aws/services/s3/s3_bucket_naming_convention
|
||||
cd prowler/providers/aws/services/s3/s3_bucket_naming_convention
|
||||
```
|
||||
|
||||
## Step 4: Write the Check Logic
|
||||
|
||||
Create `s3_bucket_naming_convention.py`:
|
||||
|
||||
```python
|
||||
from prowler.lib.check.models import Check, Check_Report_AWS
|
||||
from prowler.providers.aws.services.s3.s3_client import s3_client
|
||||
|
||||
class s3_bucket_naming_convention(Check):
|
||||
def execute(self):
|
||||
findings = []
|
||||
# Define your organization's naming pattern
|
||||
naming_pattern = "company-"
|
||||
|
||||
for bucket in s3_client.buckets:
|
||||
report = Check_Report_AWS(self.metadata())
|
||||
report.region = bucket.region
|
||||
report.resource_id = bucket.name
|
||||
report.resource_arn = bucket.arn
|
||||
report.resource_tags = bucket.tags
|
||||
|
||||
# Check if bucket name follows naming convention
|
||||
if bucket.name.startswith(naming_pattern):
|
||||
report.status = "PASS"
|
||||
report.status_extended = f"S3 bucket {bucket.name} follows naming convention."
|
||||
else:
|
||||
report.status = "FAIL"
|
||||
report.status_extended = f"S3 bucket {bucket.name} does not follow naming convention (should start with '{naming_pattern}')."
|
||||
|
||||
findings.append(report)
|
||||
|
||||
return findings
|
||||
```
|
||||
|
||||
<Tip>
|
||||
Customize the `naming_pattern` variable to match your organization's requirements (e.g., "prod-", "dev-", "projectname-").
|
||||
</Tip>
|
||||
|
||||
## Step 5: Create Check Metadata
|
||||
|
||||
Create `s3_bucket_naming_convention.metadata.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"Provider": "aws",
|
||||
"CheckID": "s3_bucket_naming_convention",
|
||||
"CheckTitle": "Check if S3 buckets follow naming convention",
|
||||
"CheckType": ["Software and Configuration Checks"],
|
||||
"ServiceName": "s3",
|
||||
"SubServiceName": "",
|
||||
"ResourceIdTemplate": "arn:aws:s3:::bucket_name",
|
||||
"Severity": "low",
|
||||
"ResourceType": "AwsS3Bucket",
|
||||
"Description": "Ensure S3 buckets follow the organization's naming convention for consistency and management.",
|
||||
"Risk": "S3 buckets not following naming conventions may lead to management difficulties and confusion.",
|
||||
"RelatedUrl": "https://docs.aws.amazon.com/AmazonS3/latest/userguide/bucketnamingrules.html",
|
||||
"Remediation": {
|
||||
"Code": {
|
||||
"CLI": "",
|
||||
"NativeIaC": "",
|
||||
"Other": "Rename the S3 bucket to follow the organization's naming convention or update bucket policies.",
|
||||
"Terraform": ""
|
||||
},
|
||||
"Recommendation": {
|
||||
"Text": "Ensure all S3 buckets follow the defined naming convention for your organization.",
|
||||
"Url": "https://docs.aws.amazon.com/AmazonS3/latest/userguide/bucketnamingrules.html"
|
||||
}
|
||||
},
|
||||
"Categories": [
|
||||
"forensics-ready"
|
||||
],
|
||||
"DependsOn": [],
|
||||
"RelatedTo": [],
|
||||
"Notes": "This is a custom check created for organization-specific requirements."
|
||||
}
|
||||
```
|
||||
|
||||
<Note>
|
||||
[Note: Screenshot of slide 33 showing metadata structure - to be added]
|
||||
</Note>
|
||||
|
||||
## Step 6: Test the Custom Check
|
||||
|
||||
Run only your custom check:
|
||||
|
||||
```bash
|
||||
prowler aws --checks s3_bucket_naming_convention
|
||||
```
|
||||
|
||||
Review the output to verify:
|
||||
* Check executes without errors
|
||||
* Findings are generated for each S3 bucket
|
||||
* Status is correct (PASS/FAIL) based on naming convention
|
||||
|
||||
## Step 7: Create a Custom Check for EC2 Instance Tags
|
||||
|
||||
Create another custom check to enforce EC2 tagging policies:
|
||||
|
||||
```bash
|
||||
mkdir -p prowler/providers/aws/services/ec2/ec2_instance_required_tags
|
||||
cd prowler/providers/aws/services/ec2/ec2_instance_required_tags
|
||||
```
|
||||
|
||||
Create `ec2_instance_required_tags.py`:
|
||||
|
||||
```python
|
||||
from prowler.lib.check.models import Check, Check_Report_AWS
|
||||
from prowler.providers.aws.services.ec2.ec2_client import ec2_client
|
||||
|
||||
class ec2_instance_required_tags(Check):
|
||||
def execute(self):
|
||||
findings = []
|
||||
# Define required tags
|
||||
required_tags = ["Environment", "Owner", "CostCenter"]
|
||||
|
||||
for instance in ec2_client.instances:
|
||||
report = Check_Report_AWS(self.metadata())
|
||||
report.region = instance.region
|
||||
report.resource_id = instance.id
|
||||
report.resource_arn = instance.arn
|
||||
report.resource_tags = instance.tags
|
||||
|
||||
# Get instance tag keys
|
||||
instance_tag_keys = [tag["Key"] for tag in instance.tags] if instance.tags else []
|
||||
|
||||
# Check if all required tags are present
|
||||
missing_tags = [tag for tag in required_tags if tag not in instance_tag_keys]
|
||||
|
||||
if not missing_tags:
|
||||
report.status = "PASS"
|
||||
report.status_extended = f"EC2 instance {instance.id} has all required tags."
|
||||
else:
|
||||
report.status = "FAIL"
|
||||
report.status_extended = f"EC2 instance {instance.id} is missing required tags: {', '.join(missing_tags)}."
|
||||
|
||||
findings.append(report)
|
||||
|
||||
return findings
|
||||
```
|
||||
|
||||
Create `ec2_instance_required_tags.metadata.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"Provider": "aws",
|
||||
"CheckID": "ec2_instance_required_tags",
|
||||
"CheckTitle": "Check if EC2 instances have required tags",
|
||||
"CheckType": ["Software and Configuration Checks"],
|
||||
"ServiceName": "ec2",
|
||||
"SubServiceName": "",
|
||||
"ResourceIdTemplate": "arn:aws:ec2:region:account-id:instance/instance-id",
|
||||
"Severity": "medium",
|
||||
"ResourceType": "AwsEc2Instance",
|
||||
"Description": "Ensure EC2 instances have required tags for proper resource management and cost allocation.",
|
||||
"Risk": "EC2 instances without required tags may lead to difficulties in cost tracking, ownership identification, and resource management.",
|
||||
"RelatedUrl": "https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/Using_Tags.html",
|
||||
"Remediation": {
|
||||
"Code": {
|
||||
"CLI": "aws ec2 create-tags --resources <instance-id> --tags Key=Environment,Value=<value> Key=Owner,Value=<value> Key=CostCenter,Value=<value>",
|
||||
"NativeIaC": "",
|
||||
"Other": "",
|
||||
"Terraform": "resource \"aws_ec2_tag\" \"example\" {\n resource_id = aws_instance.example.id\n key = \"Environment\"\n value = \"Production\"\n}"
|
||||
},
|
||||
"Recommendation": {
|
||||
"Text": "Add the required tags (Environment, Owner, CostCenter) to all EC2 instances.",
|
||||
"Url": "https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/Using_Tags.html"
|
||||
}
|
||||
},
|
||||
"Categories": [
|
||||
"tagging"
|
||||
],
|
||||
"DependsOn": [],
|
||||
"RelatedTo": [],
|
||||
"Notes": "Customize the required_tags list in the check code to match your organization's tagging policy."
|
||||
}
|
||||
```
|
||||
|
||||
## Step 8: Test Multiple Custom Checks
|
||||
|
||||
Run both custom checks together:
|
||||
|
||||
```bash
|
||||
prowler aws --checks s3_bucket_naming_convention ec2_instance_required_tags
|
||||
```
|
||||
|
||||
## Step 9: Create a Custom Checks Group
|
||||
|
||||
Create a file to group your custom checks:
|
||||
|
||||
Create `prowler/config/custom_checks.yaml`:
|
||||
|
||||
```yaml
|
||||
custom-checks:
|
||||
- s3_bucket_naming_convention
|
||||
- ec2_instance_required_tags
|
||||
```
|
||||
|
||||
Run all custom checks:
|
||||
|
||||
```bash
|
||||
prowler aws --checks-file prowler/config/custom_checks.yaml
|
||||
```
|
||||
|
||||
<Note>
|
||||
[Note: Screenshot of slide 38 showing custom checks output - to be added]
|
||||
</Note>
|
||||
|
||||
## Step 10: Validate Check Metadata
|
||||
|
||||
Prowler includes metadata validation. Ensure your metadata follows guidelines:
|
||||
|
||||
```bash
|
||||
python -m prowler.lib.check.check_metadata_validator
|
||||
```
|
||||
|
||||
This validates:
|
||||
* Required metadata fields are present
|
||||
* Severity values are valid
|
||||
* URLs are properly formatted
|
||||
* JSON structure is correct
|
||||
|
||||
## Verification Steps
|
||||
|
||||
Confirm successful lab completion:
|
||||
|
||||
1. Prowler installed from source
|
||||
2. Custom S3 naming convention check created
|
||||
3. Custom EC2 tagging check created
|
||||
4. Both checks execute successfully
|
||||
5. Metadata files are properly formatted
|
||||
6. Custom checks grouped for easy execution
|
||||
|
||||
## Expected Outcomes
|
||||
|
||||
After completing this lab, you should:
|
||||
|
||||
* Understand Prowler's check architecture
|
||||
* Be able to create custom security checks
|
||||
* Know how to write check metadata
|
||||
* Be capable of testing and validating checks
|
||||
* Have created reusable custom security policies
|
||||
|
||||
## Best Practices for Custom Checks
|
||||
|
||||
1. **Follow naming conventions:** Use descriptive check IDs (e.g., `service_resource_requirement`)
|
||||
2. **Set appropriate severity:** Match severity to the security impact
|
||||
3. **Provide clear descriptions:** Help users understand what the check validates
|
||||
4. **Include remediation guidance:** Provide actionable steps to fix findings
|
||||
5. **Test thoroughly:** Verify checks work across different AWS regions and account configurations
|
||||
6. **Document assumptions:** Note any specific requirements or limitations
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
**Issue:** Check not found when running
|
||||
* **Solution:** Ensure the check directory and files follow the correct naming convention and location
|
||||
|
||||
**Issue:** Import errors in check code
|
||||
* **Solution:** Verify you're using the Poetry virtual environment (`poetry shell`)
|
||||
|
||||
**Issue:** Metadata validation fails
|
||||
* **Solution:** Review the metadata format against Prowler's schema requirements
|
||||
|
||||
**Issue:** Check returns no findings
|
||||
* **Solution:** Add print statements or use a debugger to verify the service client has data
|
||||
|
||||
## Next Steps
|
||||
|
||||
Continue to [Lab 4: Multi-Cloud Security with Prowler (Azure)](/workshop/lab-04-azure-multicloud) to extend security monitoring to Azure environments.
|
||||
|
||||
## Additional Resources
|
||||
|
||||
* [Custom Checks Development Guide](/developer-guide/checks)
|
||||
* [Check Metadata Guidelines](/developer-guide/check-metadata-guidelines)
|
||||
* [Prowler Development Documentation](/developer-guide/introduction)
|
||||
* [Prowler Check Kreator](/user-guide/cli/tutorials/prowler-check-kreator)
|
||||
@@ -1,346 +0,0 @@
|
||||
---
|
||||
title: "Lab 4: Multi-Cloud Security with Prowler (Azure)"
|
||||
description: "Extend security monitoring to Azure environments using Prowler's multi-cloud capabilities"
|
||||
---
|
||||
|
||||
<Note>
|
||||
**Tags:** `workshop` `azure` `multi-cloud` `intermediate` `authentication`
|
||||
</Note>
|
||||
|
||||
# Lab 4: Multi-Cloud Security with Prowler (Azure)
|
||||
|
||||
Learn to secure Azure environments using Prowler's multi-cloud security assessment capabilities.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
* Prowler CLI installed ([Lab 1](/workshop/lab-01-getting-started))
|
||||
* Active Azure subscription
|
||||
* Azure CLI installed
|
||||
* Azure account with appropriate permissions (Reader role minimum)
|
||||
* Basic understanding of Azure services
|
||||
|
||||
**Estimated Time:** 45 minutes
|
||||
|
||||
## Lab Objectives
|
||||
|
||||
By completing this lab, you will:
|
||||
|
||||
* Configure Azure authentication for Prowler
|
||||
* Run security assessments on Azure subscriptions
|
||||
* Understand Azure-specific security checks
|
||||
* Compare security findings across cloud providers
|
||||
* Implement multi-cloud security strategies
|
||||
|
||||
## Step 1: Install Azure CLI
|
||||
|
||||
Install Azure CLI if not already present:
|
||||
|
||||
**macOS:**
|
||||
```bash
|
||||
brew install azure-cli
|
||||
```
|
||||
|
||||
**Linux:**
|
||||
```bash
|
||||
curl -sL https://aka.ms/InstallAzureCLIDeb | sudo bash
|
||||
```
|
||||
|
||||
**Windows:**
|
||||
```powershell
|
||||
winget install Microsoft.AzureCLI
|
||||
```
|
||||
|
||||
Verify installation:
|
||||
```bash
|
||||
az --version
|
||||
```
|
||||
|
||||
## Step 2: Authenticate to Azure
|
||||
|
||||
Sign in to Azure:
|
||||
|
||||
```bash
|
||||
az login
|
||||
```
|
||||
|
||||
This opens a browser window for authentication.
|
||||
|
||||
Verify authentication:
|
||||
```bash
|
||||
az account show
|
||||
```
|
||||
|
||||
Expected output:
|
||||
```json
|
||||
{
|
||||
"id": "12345678-1234-1234-1234-123456789012",
|
||||
"name": "My Subscription",
|
||||
"tenantId": "87654321-4321-4321-4321-210987654321",
|
||||
"state": "Enabled"
|
||||
}
|
||||
```
|
||||
|
||||
<Note>
|
||||
[Note: Screenshot of slide 43 showing Azure authentication - to be added]
|
||||
</Note>
|
||||
|
||||
## Step 3: List Azure Subscriptions
|
||||
|
||||
If you have multiple subscriptions, list them:
|
||||
|
||||
```bash
|
||||
az account list --output table
|
||||
```
|
||||
|
||||
Set the active subscription:
|
||||
```bash
|
||||
az account set --subscription "subscription-id"
|
||||
```
|
||||
|
||||
## Step 4: Configure Azure Service Principal (Optional)
|
||||
|
||||
For automated scans, create a service principal:
|
||||
|
||||
```bash
|
||||
az ad sp create-for-rbac --name "prowler-scanner" --role Reader --scopes /subscriptions/{subscription-id}
|
||||
```
|
||||
|
||||
This returns:
|
||||
```json
|
||||
{
|
||||
"appId": "app-id",
|
||||
"displayName": "prowler-scanner",
|
||||
"password": "password",
|
||||
"tenant": "tenant-id"
|
||||
}
|
||||
```
|
||||
|
||||
<Warning>
|
||||
Store service principal credentials securely. These provide programmatic access to your Azure subscription.
|
||||
</Warning>
|
||||
|
||||
Export credentials as environment variables:
|
||||
```bash
|
||||
export AZURE_CLIENT_ID="app-id"
|
||||
export AZURE_CLIENT_SECRET="password"
|
||||
export AZURE_TENANT_ID="tenant-id"
|
||||
export AZURE_SUBSCRIPTION_ID="subscription-id"
|
||||
```
|
||||
|
||||
## Step 5: Run Your First Azure Scan
|
||||
|
||||
Execute Prowler against Azure:
|
||||
|
||||
```bash
|
||||
prowler azure
|
||||
```
|
||||
|
||||
This command:
|
||||
* Uses Azure CLI credentials (or service principal if configured)
|
||||
* Scans the active subscription
|
||||
* Runs all Azure security checks
|
||||
* Generates output in multiple formats
|
||||
|
||||
<Note>
|
||||
Azure scans typically take 5-10 minutes depending on resource count.
|
||||
</Note>
|
||||
|
||||
<Note>
|
||||
[Note: Screenshot of slide 47 showing Azure scan execution - to be added]
|
||||
</Note>
|
||||
|
||||
## Step 6: Scan Specific Azure Services
|
||||
|
||||
Run targeted scans for specific services:
|
||||
|
||||
```bash
|
||||
prowler azure --services storage network
|
||||
```
|
||||
|
||||
This focuses on:
|
||||
* Azure Storage accounts
|
||||
* Virtual networks
|
||||
* Network security groups
|
||||
|
||||
## Step 7: Analyze Azure Security Findings
|
||||
|
||||
Review Azure-specific security checks:
|
||||
|
||||
**Storage Account Security:**
|
||||
* Public blob access disabled
|
||||
* Secure transfer required (HTTPS)
|
||||
* Storage encryption enabled
|
||||
* Soft delete enabled
|
||||
|
||||
**Network Security:**
|
||||
* Network security groups properly configured
|
||||
* No overly permissive rules
|
||||
* DDoS protection enabled
|
||||
* Network watcher enabled
|
||||
|
||||
**Identity and Access:**
|
||||
* Multi-factor authentication enabled
|
||||
* Conditional access policies configured
|
||||
* Privileged identity management enabled
|
||||
|
||||
Open the HTML report:
|
||||
```bash
|
||||
open output/prowler-output-azure-*.html
|
||||
```
|
||||
|
||||
<Note>
|
||||
[Note: Screenshot of slide 50 showing Azure findings report - to be added]
|
||||
</Note>
|
||||
|
||||
## Step 8: Compare AWS and Azure Security Posture
|
||||
|
||||
If you completed Lab 1, compare security findings:
|
||||
|
||||
**AWS findings:**
|
||||
```bash
|
||||
cat output/prowler-output-aws-*.csv | wc -l
|
||||
```
|
||||
|
||||
**Azure findings:**
|
||||
```bash
|
||||
cat output/prowler-output-azure-*.csv | wc -l
|
||||
```
|
||||
|
||||
Key comparison metrics:
|
||||
* Total findings by severity
|
||||
* Service coverage
|
||||
* Compliance status
|
||||
* Resource exposure
|
||||
|
||||
## Step 9: Multi-Cloud Security Dashboard
|
||||
|
||||
Generate a combined security view:
|
||||
|
||||
Create a directory for multi-cloud reports:
|
||||
```bash
|
||||
mkdir -p multi-cloud-reports
|
||||
cp output/prowler-output-aws-*.json multi-cloud-reports/
|
||||
cp output/prowler-output-azure-*.json multi-cloud-reports/
|
||||
```
|
||||
|
||||
<Tip>
|
||||
Use Prowler Cloud or custom dashboards to visualize multi-cloud security posture in a unified interface.
|
||||
</Tip>
|
||||
|
||||
## Step 10: Azure-Specific Remediation
|
||||
|
||||
Example remediations for common Azure findings:
|
||||
|
||||
**Enable secure transfer for storage account:**
|
||||
```bash
|
||||
az storage account update \
|
||||
--name mystorageaccount \
|
||||
--resource-group myresourcegroup \
|
||||
--https-only true
|
||||
```
|
||||
|
||||
**Enable storage encryption:**
|
||||
```bash
|
||||
az storage account update \
|
||||
--name mystorageaccount \
|
||||
--resource-group myresourcegroup \
|
||||
--encryption-services blob
|
||||
```
|
||||
|
||||
**Disable public blob access:**
|
||||
```bash
|
||||
az storage account update \
|
||||
--name mystorageaccount \
|
||||
--resource-group myresourcegroup \
|
||||
--allow-blob-public-access false
|
||||
```
|
||||
|
||||
**Update network security group rule:**
|
||||
```bash
|
||||
az network nsg rule update \
|
||||
--resource-group myresourcegroup \
|
||||
--nsg-name mynsg \
|
||||
--name mynsgrule \
|
||||
--source-address-prefixes 10.0.0.0/16
|
||||
```
|
||||
|
||||
## Step 11: Scan Multiple Azure Subscriptions
|
||||
|
||||
Scan all subscriptions in your tenant:
|
||||
|
||||
```bash
|
||||
prowler azure --subscription-ids subscription-id-1 subscription-id-2
|
||||
```
|
||||
|
||||
Or scan all accessible subscriptions:
|
||||
```bash
|
||||
prowler azure --az-cli-auth
|
||||
```
|
||||
|
||||
<Note>
|
||||
[Note: Screenshot of slide 56 showing multi-subscription scan - to be added]
|
||||
</Note>
|
||||
|
||||
## Verification Steps
|
||||
|
||||
Confirm successful lab completion:
|
||||
|
||||
1. Azure CLI installed and authenticated
|
||||
2. First Azure scan completed successfully
|
||||
3. Azure security findings reviewed
|
||||
4. Service-specific scans executed
|
||||
5. Multi-cloud comparison performed
|
||||
6. Azure-specific remediations understood
|
||||
|
||||
## Expected Outcomes
|
||||
|
||||
After completing this lab, you should:
|
||||
|
||||
* Be able to authenticate Prowler with Azure
|
||||
* Understand Azure security checks
|
||||
* Know how to scan multiple subscriptions
|
||||
* Have compared security posture across AWS and Azure
|
||||
* Be familiar with Azure-specific remediation commands
|
||||
|
||||
## Common Azure Security Findings
|
||||
|
||||
**Storage Accounts:**
|
||||
* Public blob access enabled
|
||||
* Secure transfer (HTTPS) not required
|
||||
* Storage encryption disabled
|
||||
* Logging not configured
|
||||
|
||||
**Virtual Networks:**
|
||||
* Network security groups allow 0.0.0.0/0 access
|
||||
* DDoS protection not enabled
|
||||
* Network watcher not configured
|
||||
|
||||
**Identity:**
|
||||
* MFA not enabled for all users
|
||||
* Guest users have excessive permissions
|
||||
* Password policies are weak
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
**Issue:** Azure authentication fails
|
||||
* **Solution:** Run `az login` and ensure you have the correct subscription selected
|
||||
|
||||
**Issue:** Permission errors during scan
|
||||
* **Solution:** Ensure your account or service principal has Reader role at subscription level
|
||||
|
||||
**Issue:** Subscription not found
|
||||
* **Solution:** Verify subscription ID with `az account list` and check it's enabled
|
||||
|
||||
**Issue:** Slow scan performance
|
||||
* **Solution:** Use `--services` flag to scan specific services instead of all
|
||||
|
||||
## Next Steps
|
||||
|
||||
Continue to [Lab 5: Multi-Cloud Security with Prowler (GCP)](/workshop/lab-05-gcp-multicloud) to add Google Cloud Platform to your multi-cloud security monitoring.
|
||||
|
||||
## Additional Resources
|
||||
|
||||
* [Azure Getting Started Guide](/user-guide/providers/azure/getting-started-azure)
|
||||
* [Azure Authentication Methods](/user-guide/providers/azure/authentication)
|
||||
* [Create Prowler Service Principal](/user-guide/providers/azure/create-prowler-service-principal)
|
||||
* [Azure Subscriptions Management](/user-guide/providers/azure/subscriptions)
|
||||
@@ -1,377 +0,0 @@
|
||||
---
|
||||
title: "Lab 5: Multi-Cloud Security with Prowler (GCP)"
|
||||
description: "Complete your multi-cloud security coverage by adding Google Cloud Platform assessments"
|
||||
---
|
||||
|
||||
<Note>
|
||||
**Tags:** `workshop` `gcp` `multi-cloud` `intermediate` `authentication`
|
||||
</Note>
|
||||
|
||||
# Lab 5: Multi-Cloud Security with Prowler (GCP)
|
||||
|
||||
Learn to secure Google Cloud Platform environments and achieve comprehensive multi-cloud security coverage with Prowler.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
* Prowler CLI installed ([Lab 1](/workshop/lab-01-getting-started))
|
||||
* Active GCP project
|
||||
* Google Cloud SDK (gcloud) installed
|
||||
* GCP account with appropriate permissions (Viewer role minimum)
|
||||
* Basic understanding of GCP services
|
||||
|
||||
**Estimated Time:** 45 minutes
|
||||
|
||||
## Lab Objectives
|
||||
|
||||
By completing this lab, you will:
|
||||
|
||||
* Configure GCP authentication for Prowler
|
||||
* Run security assessments on GCP projects
|
||||
* Understand GCP-specific security checks
|
||||
* Achieve comprehensive multi-cloud security coverage (AWS, Azure, GCP)
|
||||
* Implement unified security policies across cloud providers
|
||||
|
||||
## Step 1: Install Google Cloud SDK
|
||||
|
||||
Install gcloud CLI if not already present:
|
||||
|
||||
**macOS:**
|
||||
```bash
|
||||
brew install google-cloud-sdk
|
||||
```
|
||||
|
||||
**Linux:**
|
||||
```bash
|
||||
curl https://sdk.cloud.google.com | bash
|
||||
exec -l $SHELL
|
||||
```
|
||||
|
||||
**Windows:**
|
||||
Download and install from: https://cloud.google.com/sdk/docs/install
|
||||
|
||||
Verify installation:
|
||||
```bash
|
||||
gcloud --version
|
||||
```
|
||||
|
||||
## Step 2: Authenticate to GCP
|
||||
|
||||
Initialize gcloud and authenticate:
|
||||
|
||||
```bash
|
||||
gcloud init
|
||||
```
|
||||
|
||||
This prompts you to:
|
||||
1. Log in to your Google account
|
||||
2. Select or create a GCP project
|
||||
3. Configure default region/zone (optional)
|
||||
|
||||
Verify authentication:
|
||||
```bash
|
||||
gcloud auth list
|
||||
```
|
||||
|
||||
Display active project:
|
||||
```bash
|
||||
gcloud config get-value project
|
||||
```
|
||||
|
||||
<Note>
|
||||
[Note: Screenshot of slide 60 showing GCP authentication - to be added]
|
||||
</Note>
|
||||
|
||||
## Step 3: Configure Application Default Credentials
|
||||
|
||||
Prowler uses Application Default Credentials (ADC):
|
||||
|
||||
```bash
|
||||
gcloud auth application-default login
|
||||
```
|
||||
|
||||
This creates credentials file at:
|
||||
* **Linux/macOS:** `~/.config/gcloud/application_default_credentials.json`
|
||||
* **Windows:** `%APPDATA%\gcloud\application_default_credentials.json`
|
||||
|
||||
## Step 4: Set Up Service Account (Optional)
|
||||
|
||||
For automated scans, create a service account:
|
||||
|
||||
```bash
|
||||
# Create service account
|
||||
gcloud iam service-accounts create prowler-scanner \
|
||||
--display-name="Prowler Security Scanner"
|
||||
|
||||
# Get project ID
|
||||
PROJECT_ID=$(gcloud config get-value project)
|
||||
|
||||
# Grant Viewer role
|
||||
gcloud projects add-iam-policy-binding $PROJECT_ID \
|
||||
--member="serviceAccount:prowler-scanner@${PROJECT_ID}.iam.gserviceaccount.com" \
|
||||
--role="roles/viewer"
|
||||
|
||||
# Generate key file
|
||||
gcloud iam service-accounts keys create ~/prowler-credentials.json \
|
||||
--iam-account=prowler-scanner@${PROJECT_ID}.iam.gserviceaccount.com
|
||||
```
|
||||
|
||||
<Warning>
|
||||
Store service account key files securely. These provide programmatic access to your GCP project.
|
||||
</Warning>
|
||||
|
||||
Use service account credentials:
|
||||
```bash
|
||||
export GOOGLE_APPLICATION_CREDENTIALS=~/prowler-credentials.json
|
||||
```
|
||||
|
||||
## Step 5: Run Your First GCP Scan
|
||||
|
||||
Execute Prowler against GCP:
|
||||
|
||||
```bash
|
||||
prowler gcp
|
||||
```
|
||||
|
||||
This command:
|
||||
* Uses Application Default Credentials (or service account)
|
||||
* Scans the active project
|
||||
* Runs all GCP security checks
|
||||
* Generates output in multiple formats
|
||||
|
||||
<Note>
|
||||
GCP scans typically take 5-10 minutes depending on resource count.
|
||||
</Note>
|
||||
|
||||
<Note>
|
||||
[Note: Screenshot of slide 65 showing GCP scan execution - to be added]
|
||||
</Note>
|
||||
|
||||
## Step 6: Scan Specific GCP Projects
|
||||
|
||||
Scan a specific project:
|
||||
|
||||
```bash
|
||||
prowler gcp --project-id my-project-id
|
||||
```
|
||||
|
||||
Scan multiple projects:
|
||||
```bash
|
||||
prowler gcp --project-id project-1 project-2 project-3
|
||||
```
|
||||
|
||||
## Step 7: Scan Specific GCP Services
|
||||
|
||||
Run targeted scans for specific services:
|
||||
|
||||
```bash
|
||||
prowler gcp --services storage compute iam
|
||||
```
|
||||
|
||||
This focuses on:
|
||||
* Cloud Storage buckets
|
||||
* Compute Engine instances
|
||||
* IAM policies and permissions
|
||||
|
||||
## Step 8: Analyze GCP Security Findings
|
||||
|
||||
Review GCP-specific security checks:
|
||||
|
||||
**Cloud Storage Security:**
|
||||
* Buckets not publicly accessible
|
||||
* Uniform bucket-level access enabled
|
||||
* Encryption at rest enabled
|
||||
* Versioning enabled
|
||||
|
||||
**Compute Engine Security:**
|
||||
* OS Login enabled
|
||||
* Serial port access disabled
|
||||
* Shielded VMs enabled
|
||||
* IP forwarding disabled
|
||||
|
||||
**IAM Security:**
|
||||
* Service accounts with minimal permissions
|
||||
* No primitive roles (Owner, Editor, Viewer) assigned to users
|
||||
* Service account keys rotated regularly
|
||||
* Cloud Identity-Aware Proxy (IAP) enabled
|
||||
|
||||
Open the HTML report:
|
||||
```bash
|
||||
open output/prowler-output-gcp-*.html
|
||||
```
|
||||
|
||||
<Note>
|
||||
[Note: Screenshot of slide 69 showing GCP findings report - to be added]
|
||||
</Note>
|
||||
|
||||
## Step 9: Multi-Cloud Security Overview
|
||||
|
||||
You now have security coverage across three major cloud providers:
|
||||
|
||||
Create a comprehensive multi-cloud report directory:
|
||||
|
||||
```bash
|
||||
mkdir -p multi-cloud-security-reports
|
||||
cp output/prowler-output-aws-*.json multi-cloud-security-reports/
|
||||
cp output/prowler-output-azure-*.json multi-cloud-security-reports/
|
||||
cp output/prowler-output-gcp-*.json multi-cloud-security-reports/
|
||||
```
|
||||
|
||||
Compare security posture metrics:
|
||||
|
||||
```bash
|
||||
# Count findings by provider
|
||||
echo "AWS findings:"
|
||||
jq '.findings | length' multi-cloud-security-reports/prowler-output-aws-*.json
|
||||
|
||||
echo "Azure findings:"
|
||||
jq '.findings | length' multi-cloud-security-reports/prowler-output-azure-*.json
|
||||
|
||||
echo "GCP findings:"
|
||||
jq '.findings | length' multi-cloud-security-reports/prowler-output-gcp-*.json
|
||||
```
|
||||
|
||||
## Step 10: GCP-Specific Remediation
|
||||
|
||||
Example remediations for common GCP findings:
|
||||
|
||||
**Enable uniform bucket-level access:**
|
||||
```bash
|
||||
gsutil uniformbucketlevelaccess set on gs://bucket-name
|
||||
```
|
||||
|
||||
**Disable public access to bucket:**
|
||||
```bash
|
||||
gsutil iam ch -d allUsers gs://bucket-name
|
||||
gsutil iam ch -d allAuthenticatedUsers gs://bucket-name
|
||||
```
|
||||
|
||||
**Enable OS Login on project:**
|
||||
```bash
|
||||
gcloud compute project-info add-metadata \
|
||||
--metadata enable-oslogin=TRUE
|
||||
```
|
||||
|
||||
**Disable serial port access:**
|
||||
```bash
|
||||
gcloud compute instances add-metadata instance-name \
|
||||
--metadata serial-port-enable=FALSE
|
||||
```
|
||||
|
||||
**Remove primitive role binding:**
|
||||
```bash
|
||||
gcloud projects remove-iam-policy-binding PROJECT_ID \
|
||||
--member='user:email@example.com' \
|
||||
--role='roles/editor'
|
||||
```
|
||||
|
||||
## Step 11: Scan GCP Organization
|
||||
|
||||
If you have organization-level access:
|
||||
|
||||
```bash
|
||||
prowler gcp --organization-id org-id
|
||||
```
|
||||
|
||||
This scans all projects within the organization.
|
||||
|
||||
<Tip>
|
||||
Organization-level scanning requires `resourcemanager.organizations.get` permission at the organization level.
|
||||
</Tip>
|
||||
|
||||
<Note>
|
||||
[Note: Screenshot of slide 74 showing organization scan - to be added]
|
||||
</Note>
|
||||
|
||||
## Step 12: Multi-Cloud Security Strategy
|
||||
|
||||
Establish consistent security controls across clouds:
|
||||
|
||||
**Identity and Access:**
|
||||
* Enforce MFA across all providers
|
||||
* Implement least privilege access
|
||||
* Regular access reviews
|
||||
* Centralized identity management
|
||||
|
||||
**Data Protection:**
|
||||
* Encryption at rest and in transit
|
||||
* Regular backups
|
||||
* Data retention policies
|
||||
* Access logging enabled
|
||||
|
||||
**Network Security:**
|
||||
* Zero-trust network architecture
|
||||
* Network segmentation
|
||||
* DDoS protection
|
||||
* Traffic inspection
|
||||
|
||||
**Monitoring and Compliance:**
|
||||
* Centralized logging
|
||||
* Security information and event management (SIEM)
|
||||
* Regular compliance scans
|
||||
* Automated remediation where possible
|
||||
|
||||
## Verification Steps
|
||||
|
||||
Confirm successful lab completion:
|
||||
|
||||
1. Google Cloud SDK installed and authenticated
|
||||
2. First GCP scan completed successfully
|
||||
3. GCP security findings reviewed
|
||||
4. Service-specific scans executed
|
||||
5. Multi-cloud reports collected (AWS, Azure, GCP)
|
||||
6. GCP-specific remediations understood
|
||||
|
||||
## Expected Outcomes
|
||||
|
||||
After completing this lab, you should:
|
||||
|
||||
* Be able to authenticate Prowler with GCP
|
||||
* Understand GCP security checks
|
||||
* Know how to scan multiple projects and organizations
|
||||
* Have achieved multi-cloud security coverage
|
||||
* Be familiar with GCP-specific remediation commands
|
||||
|
||||
## Common GCP Security Findings
|
||||
|
||||
**Cloud Storage:**
|
||||
* Buckets with public access
|
||||
* Uniform bucket-level access not enabled
|
||||
* Versioning disabled
|
||||
* Logging not configured
|
||||
|
||||
**Compute Engine:**
|
||||
* OS Login not enabled
|
||||
* Legacy metadata endpoints enabled
|
||||
* Serial port access enabled
|
||||
* IP forwarding enabled on instances
|
||||
|
||||
**IAM:**
|
||||
* Primitive roles assigned to users
|
||||
* Service account keys not rotated
|
||||
* Over-permissive service accounts
|
||||
* No organization policies enforced
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
**Issue:** GCP authentication fails
|
||||
* **Solution:** Run `gcloud auth application-default login` and ensure project is set
|
||||
|
||||
**Issue:** Permission errors during scan
|
||||
* **Solution:** Ensure account has Viewer role at project or organization level
|
||||
|
||||
**Issue:** Project not found
|
||||
* **Solution:** Verify project ID with `gcloud projects list` and check it's active
|
||||
|
||||
**Issue:** API not enabled errors
|
||||
* **Solution:** Enable required APIs: `gcloud services enable cloudresourcemanager.googleapis.com`
|
||||
|
||||
## Next Steps
|
||||
|
||||
Continue to [Lab 6: Compliance as Code with Prowler](/workshop/lab-06-compliance-as-code) to learn how to automate compliance reporting across all cloud providers.
|
||||
|
||||
## Additional Resources
|
||||
|
||||
* [GCP Getting Started Guide](/user-guide/providers/gcp/getting-started-gcp)
|
||||
* [GCP Authentication Methods](/user-guide/providers/gcp/authentication)
|
||||
* [GCP Projects Management](/user-guide/providers/gcp/projects)
|
||||
* [GCP Organization Scanning](/user-guide/providers/gcp/organization)
|
||||
@@ -1,465 +0,0 @@
|
||||
---
|
||||
title: "Lab 6: Compliance as Code with Prowler"
|
||||
description: "Automate compliance reporting and validation against industry standards and regulatory frameworks"
|
||||
---
|
||||
|
||||
<Note>
|
||||
**Tags:** `workshop` `aws` `compliance` `intermediate` `automation` `frameworks`
|
||||
</Note>
|
||||
|
||||
# Lab 6: Compliance as Code with Prowler
|
||||
|
||||
Learn to automate compliance validation and reporting against industry standards such as CIS, PCI-DSS, HIPAA, and custom compliance frameworks.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
* Completion of [Lab 1: Getting Started with Prowler CLI](/workshop/lab-01-getting-started)
|
||||
* AWS account with resources
|
||||
* Prowler CLI installed and configured
|
||||
* Understanding of compliance frameworks (CIS, PCI-DSS, HIPAA)
|
||||
|
||||
**Estimated Time:** 50 minutes
|
||||
|
||||
## Lab Objectives
|
||||
|
||||
By completing this lab, you will:
|
||||
|
||||
* Understand compliance frameworks in Prowler
|
||||
* Generate compliance reports for industry standards
|
||||
* Validate compliance status programmatically
|
||||
* Create custom compliance frameworks
|
||||
* Automate compliance reporting in CI/CD pipelines
|
||||
|
||||
## Step 1: List Available Compliance Frameworks
|
||||
|
||||
View all supported compliance frameworks:
|
||||
|
||||
```bash
|
||||
prowler aws --list-compliance
|
||||
```
|
||||
|
||||
This displays frameworks such as:
|
||||
* CIS AWS Foundations Benchmark (multiple versions)
|
||||
* PCI-DSS v4.0
|
||||
* HIPAA
|
||||
* SOC2
|
||||
* GDPR
|
||||
* ISO 27001
|
||||
* NIST 800-53
|
||||
* AWS Foundational Security Best Practices
|
||||
* Custom frameworks
|
||||
|
||||
<Note>
|
||||
[Note: Screenshot of slide 78 showing compliance frameworks list - to be added]
|
||||
</Note>
|
||||
|
||||
## Step 2: Run CIS Benchmark Compliance Scan
|
||||
|
||||
Execute a CIS AWS Foundations Benchmark scan:
|
||||
|
||||
```bash
|
||||
prowler aws --compliance cis_2.0_aws
|
||||
```
|
||||
|
||||
This command:
|
||||
* Runs only checks mapped to CIS Benchmark v2.0
|
||||
* Generates a compliance report
|
||||
* Shows compliance percentage
|
||||
* Identifies non-compliant controls
|
||||
|
||||
Review compliance summary:
|
||||
```bash
|
||||
open output/compliance/prowler-compliance-cis_2.0_aws-*.html
|
||||
```
|
||||
|
||||
<Note>
|
||||
[Note: Screenshot of slide 80 showing CIS compliance report - to be added]
|
||||
</Note>
|
||||
|
||||
## Step 3: Analyze Compliance Requirements
|
||||
|
||||
Understanding compliance report structure:
|
||||
|
||||
**Requirement ID:** Control identifier (e.g., 1.1, 1.2)
|
||||
**Requirement Description:** What the control validates
|
||||
**Status:** PASS or FAIL
|
||||
**Related Checks:** Prowler checks that map to this requirement
|
||||
**Resources Affected:** Specific resources that failed
|
||||
|
||||
Example CIS requirement:
|
||||
|
||||
```
|
||||
ID: 1.4
|
||||
Description: Ensure no root account access key exists
|
||||
Status: FAIL
|
||||
Checks: iam_root_user_no_access_keys
|
||||
Resources: Root account has 1 active access key
|
||||
```
|
||||
|
||||
## Step 4: Generate Multiple Compliance Reports
|
||||
|
||||
Run scans for multiple frameworks:
|
||||
|
||||
```bash
|
||||
prowler aws --compliance cis_2.0_aws pci_dss_v4.0_aws hipaa_aws
|
||||
```
|
||||
|
||||
This generates three separate compliance reports:
|
||||
* `prowler-compliance-cis_2.0_aws-*.html`
|
||||
* `prowler-compliance-pci_dss_v4.0_aws-*.html`
|
||||
* `prowler-compliance-hipaa_aws-*.html`
|
||||
|
||||
Compare compliance posture across frameworks:
|
||||
```bash
|
||||
grep "Compliance Status" output/compliance/*.html
|
||||
```
|
||||
|
||||
## Step 5: Export Compliance Data
|
||||
|
||||
Export compliance results to JSON for automation:
|
||||
|
||||
```bash
|
||||
prowler aws --compliance cis_2.0_aws -o json-ocsf
|
||||
```
|
||||
|
||||
The JSON output includes:
|
||||
* Compliance score (percentage)
|
||||
* Passed requirements
|
||||
* Failed requirements
|
||||
* Resource-level details
|
||||
* Remediation guidance
|
||||
|
||||
Query compliance status programmatically:
|
||||
```bash
|
||||
jq '.compliance.cis_2.0_aws.score' output/prowler-output-*.json-ocsf
|
||||
```
|
||||
|
||||
<Note>
|
||||
[Note: Screenshot of slide 84 showing JSON compliance output - to be added]
|
||||
</Note>
|
||||
|
||||
## Step 6: Create a Custom Compliance Framework
|
||||
|
||||
Create a custom framework for organization-specific requirements:
|
||||
|
||||
Create `custom_compliance.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"Framework": "custom_security_baseline",
|
||||
"Version": "1.0",
|
||||
"Provider": "aws",
|
||||
"Description": "Organization Security Baseline Requirements",
|
||||
"Requirements": [
|
||||
{
|
||||
"Id": "1.1",
|
||||
"Description": "S3 buckets must have encryption enabled",
|
||||
"Attributes": [
|
||||
{
|
||||
"Section": "Data Protection",
|
||||
"SubSection": "Encryption at Rest",
|
||||
"Type": "automated",
|
||||
"Service": "s3"
|
||||
}
|
||||
],
|
||||
"Checks": [
|
||||
"s3_bucket_default_encryption",
|
||||
"s3_bucket_secure_transport_policy"
|
||||
]
|
||||
},
|
||||
{
|
||||
"Id": "1.2",
|
||||
"Description": "CloudTrail must be enabled in all regions",
|
||||
"Attributes": [
|
||||
{
|
||||
"Section": "Logging and Monitoring",
|
||||
"SubSection": "Audit Logging",
|
||||
"Type": "automated",
|
||||
"Service": "cloudtrail"
|
||||
}
|
||||
],
|
||||
"Checks": [
|
||||
"cloudtrail_multi_region_enabled",
|
||||
"cloudtrail_log_file_validation_enabled"
|
||||
]
|
||||
},
|
||||
{
|
||||
"Id": "2.1",
|
||||
"Description": "IAM users must have MFA enabled",
|
||||
"Attributes": [
|
||||
{
|
||||
"Section": "Identity and Access Management",
|
||||
"SubSection": "Multi-Factor Authentication",
|
||||
"Type": "automated",
|
||||
"Service": "iam"
|
||||
}
|
||||
],
|
||||
"Checks": [
|
||||
"iam_user_mfa_enabled_console_access",
|
||||
"iam_root_mfa_enabled"
|
||||
]
|
||||
},
|
||||
{
|
||||
"Id": "3.1",
|
||||
"Description": "Security groups must not allow unrestricted access",
|
||||
"Attributes": [
|
||||
{
|
||||
"Section": "Network Security",
|
||||
"SubSection": "Firewall Rules",
|
||||
"Type": "automated",
|
||||
"Service": "ec2"
|
||||
}
|
||||
],
|
||||
"Checks": [
|
||||
"ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_22",
|
||||
"ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_3389"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Save to `prowler/compliance/aws/`:
|
||||
```bash
|
||||
cp custom_compliance.json ~/.prowler/compliance/aws/
|
||||
```
|
||||
|
||||
## Step 7: Run Custom Compliance Framework
|
||||
|
||||
Execute scan against custom framework:
|
||||
|
||||
```bash
|
||||
prowler aws --compliance-framework custom_compliance.json
|
||||
```
|
||||
|
||||
Or if placed in Prowler's compliance directory:
|
||||
```bash
|
||||
prowler aws --compliance custom_security_baseline
|
||||
```
|
||||
|
||||
Review custom compliance report:
|
||||
```bash
|
||||
open output/compliance/prowler-compliance-custom_security_baseline-*.html
|
||||
```
|
||||
|
||||
<Note>
|
||||
[Note: Screenshot of slide 88 showing custom compliance report - to be added]
|
||||
</Note>
|
||||
|
||||
## Step 8: Compliance Reporting for Audits
|
||||
|
||||
Generate audit-ready compliance reports:
|
||||
|
||||
```bash
|
||||
prowler aws \
|
||||
--compliance cis_2.0_aws \
|
||||
-o html csv json \
|
||||
--output-directory ./audit-reports-$(date +%Y%m%d)
|
||||
```
|
||||
|
||||
This creates:
|
||||
* HTML report for human review
|
||||
* CSV for spreadsheet analysis
|
||||
* JSON for programmatic processing
|
||||
|
||||
Package for auditors:
|
||||
```bash
|
||||
tar -czf compliance-audit-$(date +%Y%m%d).tar.gz audit-reports-*
|
||||
```
|
||||
|
||||
## Step 9: Automate Compliance Validation
|
||||
|
||||
Create a compliance validation script:
|
||||
|
||||
Create `compliance-check.sh`:
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
|
||||
# Configuration
|
||||
COMPLIANCE_FRAMEWORK="cis_2.0_aws"
|
||||
REQUIRED_SCORE=85
|
||||
OUTPUT_DIR="./compliance-reports"
|
||||
|
||||
# Run Prowler
|
||||
prowler aws \
|
||||
--compliance $COMPLIANCE_FRAMEWORK \
|
||||
-o json \
|
||||
--output-directory $OUTPUT_DIR
|
||||
|
||||
# Extract compliance score
|
||||
SCORE=$(jq -r ".compliance.${COMPLIANCE_FRAMEWORK}.score" \
|
||||
$OUTPUT_DIR/prowler-output-*.json | head -1)
|
||||
|
||||
echo "Compliance Score: ${SCORE}%"
|
||||
|
||||
# Validate compliance threshold
|
||||
if (( $(echo "$SCORE >= $REQUIRED_SCORE" | bc -l) )); then
|
||||
echo "✓ Compliance check PASSED (score: ${SCORE}% >= ${REQUIRED_SCORE}%)"
|
||||
exit 0
|
||||
else
|
||||
echo "✗ Compliance check FAILED (score: ${SCORE}% < ${REQUIRED_SCORE}%)"
|
||||
exit 1
|
||||
fi
|
||||
```
|
||||
|
||||
Make executable:
|
||||
```bash
|
||||
chmod +x compliance-check.sh
|
||||
```
|
||||
|
||||
Run validation:
|
||||
```bash
|
||||
./compliance-check.sh
|
||||
```
|
||||
|
||||
## Step 10: Integrate with CI/CD Pipeline
|
||||
|
||||
Example GitHub Actions workflow:
|
||||
|
||||
Create `.github/workflows/compliance-check.yml`:
|
||||
|
||||
```yaml
|
||||
name: Compliance Validation
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '0 0 * * *' # Daily at midnight
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
prowler-compliance:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: '3.11'
|
||||
|
||||
- name: Install Prowler
|
||||
run: pip install prowler
|
||||
|
||||
- name: Configure AWS credentials
|
||||
uses: aws-actions/configure-aws-credentials@v2
|
||||
with:
|
||||
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
|
||||
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
|
||||
aws-region: us-east-1
|
||||
|
||||
- name: Run compliance scan
|
||||
run: |
|
||||
prowler aws \
|
||||
--compliance cis_2.0_aws \
|
||||
-o html json \
|
||||
--output-directory ./reports
|
||||
|
||||
- name: Upload compliance reports
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: compliance-reports
|
||||
path: ./reports/
|
||||
|
||||
- name: Check compliance threshold
|
||||
run: |
|
||||
SCORE=$(jq -r '.compliance.cis_2.0_aws.score' reports/prowler-output-*.json)
|
||||
if (( $(echo "$SCORE < 85" | bc -l) )); then
|
||||
echo "Compliance score ${SCORE}% is below threshold"
|
||||
exit 1
|
||||
fi
|
||||
```
|
||||
|
||||
<Note>
|
||||
[Note: Screenshot of slide 92 showing CI/CD integration - to be added]
|
||||
</Note>
|
||||
|
||||
## Step 11: Continuous Compliance Monitoring
|
||||
|
||||
Implement continuous compliance monitoring:
|
||||
|
||||
**Daily Scans:**
|
||||
* Schedule automated scans
|
||||
* Track compliance trends over time
|
||||
* Alert on compliance score drops
|
||||
|
||||
**Drift Detection:**
|
||||
* Compare current state vs. baseline
|
||||
* Identify new non-compliant resources
|
||||
* Generate remediation tickets automatically
|
||||
|
||||
**Compliance Dashboard:**
|
||||
* Visualize compliance status
|
||||
* Track remediation progress
|
||||
* Generate executive reports
|
||||
|
||||
## Verification Steps
|
||||
|
||||
Confirm successful lab completion:
|
||||
|
||||
1. Listed available compliance frameworks
|
||||
2. Generated CIS compliance report
|
||||
3. Created multiple framework reports
|
||||
4. Built custom compliance framework
|
||||
5. Automated compliance validation
|
||||
6. Integrated compliance checks in CI/CD
|
||||
|
||||
## Expected Outcomes
|
||||
|
||||
After completing this lab, you should:
|
||||
|
||||
* Understand Prowler compliance capabilities
|
||||
* Be able to generate compliance reports
|
||||
* Know how to create custom frameworks
|
||||
* Have automated compliance validation
|
||||
* Be ready for audit processes
|
||||
|
||||
## Compliance Framework Mapping
|
||||
|
||||
Common frameworks supported:
|
||||
|
||||
**AWS:**
|
||||
* CIS AWS Foundations Benchmark v1.4, v1.5, v2.0, v3.0
|
||||
* AWS Foundational Security Best Practices
|
||||
* PCI-DSS v4.0
|
||||
* HIPAA
|
||||
* SOC2
|
||||
* GDPR
|
||||
* ISO 27001
|
||||
* NIST 800-53
|
||||
* FedRAMP
|
||||
* ENS (Spanish National Security Scheme)
|
||||
|
||||
**Azure:**
|
||||
* CIS Microsoft Azure Foundations Benchmark
|
||||
* Azure Security Benchmark
|
||||
|
||||
**GCP:**
|
||||
* CIS Google Cloud Platform Foundation Benchmark
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
**Issue:** Compliance framework not found
|
||||
* **Solution:** Use `--list-compliance` to see exact framework names
|
||||
|
||||
**Issue:** Low compliance score
|
||||
* **Solution:** Review failed checks and prioritize remediation by severity
|
||||
|
||||
**Issue:** Missing compliance report
|
||||
* **Solution:** Check `output/compliance/` directory for framework-specific reports
|
||||
|
||||
**Issue:** Custom framework not loading
|
||||
* **Solution:** Validate JSON syntax and ensure file is in correct directory
|
||||
|
||||
## Next Steps
|
||||
|
||||
Continue to [Lab 7: Integrations with Prowler](/workshop/lab-07-integrations) to learn how to integrate Prowler with AWS Security Hub and other security tools.
|
||||
|
||||
## Additional Resources
|
||||
|
||||
* [Compliance Reporting Guide](/user-guide/cli/tutorials/compliance)
|
||||
* [Compliance Frameworks Documentation](/user-guide/cli/tutorials/compliance)
|
||||
* [Custom Compliance Framework Guide](/developer-guide/security-compliance-framework)
|
||||
* [Prowler Hub Compliance Frameworks](https://hub.prowler.com/compliance)
|
||||
@@ -1,425 +0,0 @@
|
||||
---
|
||||
title: "Lab 7: Integrations with Prowler"
|
||||
description: "Integrate Prowler findings with AWS Security Hub and other security tools for centralized security management"
|
||||
---
|
||||
|
||||
<Note>
|
||||
**Tags:** `workshop` `aws` `integrations` `intermediate` `security-hub` `automation`
|
||||
</Note>
|
||||
|
||||
# Lab 7: Integrations with Prowler
|
||||
|
||||
Learn to integrate Prowler with AWS Security Hub and other security tools to centralize security findings and automate remediation workflows.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
* Completion of [Lab 1: Getting Started with Prowler CLI](/workshop/lab-01-getting-started)
|
||||
* AWS account with Security Hub enabled
|
||||
* IAM permissions for Security Hub operations
|
||||
* Prowler CLI installed and configured
|
||||
* Basic understanding of AWS Security Hub
|
||||
|
||||
**Estimated Time:** 45 minutes
|
||||
|
||||
## Lab Objectives
|
||||
|
||||
By completing this lab, you will:
|
||||
|
||||
* Enable AWS Security Hub integration
|
||||
* Send Prowler findings to Security Hub
|
||||
* Understand finding formats and mapping
|
||||
* Configure automated finding synchronization
|
||||
* Integrate with third-party security tools
|
||||
* Implement centralized security dashboards
|
||||
|
||||
## Step 1: Enable AWS Security Hub
|
||||
|
||||
Enable Security Hub in your AWS account:
|
||||
|
||||
**Via AWS Console:**
|
||||
1. Navigate to AWS Security Hub
|
||||
2. Click "Go to Security Hub"
|
||||
3. Click "Enable Security Hub"
|
||||
|
||||
**Via AWS CLI:**
|
||||
```bash
|
||||
aws securityhub enable-security-hub
|
||||
```
|
||||
|
||||
Verify Security Hub is enabled:
|
||||
```bash
|
||||
aws securityhub get-enabled-standards
|
||||
```
|
||||
|
||||
<Note>
|
||||
[Note: Screenshot of slide 96 showing Security Hub enablement - to be added]
|
||||
</Note>
|
||||
|
||||
## Step 2: Configure IAM Permissions
|
||||
|
||||
Ensure your IAM role/user has Security Hub permissions:
|
||||
|
||||
Required permissions:
|
||||
```json
|
||||
{
|
||||
"Version": "2012-10-17",
|
||||
"Statement": [
|
||||
{
|
||||
"Effect": "Allow",
|
||||
"Action": [
|
||||
"securityhub:BatchImportFindings",
|
||||
"securityhub:GetFindings"
|
||||
],
|
||||
"Resource": "*"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Create and attach policy:
|
||||
```bash
|
||||
aws iam create-policy \
|
||||
--policy-name ProwlerSecurityHubIntegration \
|
||||
--policy-document file://securityhub-policy.json
|
||||
|
||||
aws iam attach-user-policy \
|
||||
--user-name prowler-user \
|
||||
--policy-arn arn:aws:iam::ACCOUNT_ID:policy/ProwlerSecurityHubIntegration
|
||||
```
|
||||
|
||||
## Step 3: Run Prowler with Security Hub Integration
|
||||
|
||||
Execute Prowler and send findings to Security Hub:
|
||||
|
||||
```bash
|
||||
prowler aws --security-hub
|
||||
```
|
||||
|
||||
This command:
|
||||
* Runs all security checks
|
||||
* Transforms findings to AWS Security Finding Format (ASFF)
|
||||
* Sends findings to Security Hub via `BatchImportFindings` API
|
||||
* Generates local reports
|
||||
|
||||
<Warning>
|
||||
Security Hub has API rate limits. For large environments, findings are sent in batches automatically.
|
||||
</Warning>
|
||||
|
||||
<Note>
|
||||
[Note: Screenshot of slide 99 showing Prowler sending findings to Security Hub - to be added]
|
||||
</Note>
|
||||
|
||||
## Step 4: View Findings in Security Hub
|
||||
|
||||
Navigate to AWS Security Hub console and review Prowler findings:
|
||||
|
||||
**Filter by Product:**
|
||||
1. Go to "Findings" in Security Hub
|
||||
2. Add filter: `Product name is Prowler`
|
||||
3. Review findings by severity
|
||||
|
||||
**View Finding Details:**
|
||||
* Severity (CRITICAL, HIGH, MEDIUM, LOW, INFORMATIONAL)
|
||||
* Affected resource
|
||||
* Compliance framework mapping
|
||||
* Remediation guidance
|
||||
* Workflow status
|
||||
|
||||
<Note>
|
||||
[Note: Screenshot of slide 101 showing Security Hub findings view - to be added]
|
||||
</Note>
|
||||
|
||||
## Step 5: Understanding ASFF Mapping
|
||||
|
||||
Prowler findings are mapped to AWS Security Finding Format:
|
||||
|
||||
**Prowler Status → Security Hub Compliance Status:**
|
||||
* PASS → PASSED
|
||||
* FAIL → FAILED
|
||||
* MANUAL → NOT_AVAILABLE
|
||||
|
||||
**Prowler Severity → Security Hub Severity:**
|
||||
* critical → CRITICAL (90-100)
|
||||
* high → HIGH (70-89)
|
||||
* medium → MEDIUM (40-69)
|
||||
* low → LOW (1-39)
|
||||
* informational → INFORMATIONAL (0)
|
||||
|
||||
Example ASFF finding structure:
|
||||
```json
|
||||
{
|
||||
"SchemaVersion": "2018-10-08",
|
||||
"Id": "prowler-aws/account/region/check/resource",
|
||||
"ProductArn": "arn:aws:securityhub:region::product/prowler/prowler",
|
||||
"GeneratorId": "prowler-check-id",
|
||||
"AwsAccountId": "123456789012",
|
||||
"Types": ["Software and Configuration Checks"],
|
||||
"CreatedAt": "2024-01-01T00:00:00.000Z",
|
||||
"UpdatedAt": "2024-01-01T00:00:00.000Z",
|
||||
"Severity": {
|
||||
"Label": "HIGH"
|
||||
},
|
||||
"Title": "Check title",
|
||||
"Description": "Check description",
|
||||
"Resources": [
|
||||
{
|
||||
"Type": "AwsS3Bucket",
|
||||
"Id": "arn:aws:s3:::bucket-name"
|
||||
}
|
||||
],
|
||||
"Compliance": {
|
||||
"Status": "FAILED"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Step 6: Update Existing Findings
|
||||
|
||||
Run subsequent scans to update Security Hub findings:
|
||||
|
||||
```bash
|
||||
prowler aws --security-hub
|
||||
```
|
||||
|
||||
Prowler automatically:
|
||||
* Updates existing findings (same resource, same check)
|
||||
* Marks remediated issues as PASSED
|
||||
* Creates new findings for new resources
|
||||
* Archives findings for deleted resources
|
||||
|
||||
## Step 7: Regional Security Hub Integration
|
||||
|
||||
Send findings to Security Hub in specific regions:
|
||||
|
||||
```bash
|
||||
prowler aws --security-hub --region us-east-1 us-west-2
|
||||
```
|
||||
|
||||
Or enable aggregation in a single region:
|
||||
|
||||
```bash
|
||||
# Configure finding aggregator in Security Hub
|
||||
aws securityhub create-finding-aggregator \
|
||||
--region-linking-mode ALL_REGIONS
|
||||
```
|
||||
|
||||
<Tip>
|
||||
Use Security Hub finding aggregation to centralize findings from multiple regions in a single dashboard.
|
||||
</Tip>
|
||||
|
||||
## Step 8: Filter Findings Sent to Security Hub
|
||||
|
||||
Send only critical and high-severity findings:
|
||||
|
||||
```bash
|
||||
prowler aws --security-hub --severity critical high
|
||||
```
|
||||
|
||||
Send findings for specific compliance frameworks:
|
||||
|
||||
```bash
|
||||
prowler aws --security-hub --compliance cis_2.0_aws
|
||||
```
|
||||
|
||||
## Step 9: Integrate with S3 for Long-Term Storage
|
||||
|
||||
Store Prowler reports in S3 alongside Security Hub integration:
|
||||
|
||||
```bash
|
||||
prowler aws \
|
||||
--security-hub \
|
||||
-o html json csv \
|
||||
--output-bucket-no-assume s3://my-security-reports-bucket
|
||||
```
|
||||
|
||||
This enables:
|
||||
* Long-term retention of historical reports
|
||||
* Compliance audit trails
|
||||
* Trend analysis over time
|
||||
* Cost-effective storage
|
||||
|
||||
Configure S3 bucket lifecycle policies:
|
||||
```bash
|
||||
aws s3api put-bucket-lifecycle-configuration \
|
||||
--bucket my-security-reports-bucket \
|
||||
--lifecycle-configuration file://lifecycle.json
|
||||
```
|
||||
|
||||
`lifecycle.json`:
|
||||
```json
|
||||
{
|
||||
"Rules": [
|
||||
{
|
||||
"Id": "ArchiveOldReports",
|
||||
"Status": "Enabled",
|
||||
"Transitions": [
|
||||
{
|
||||
"Days": 90,
|
||||
"StorageClass": "GLACIER"
|
||||
}
|
||||
],
|
||||
"Expiration": {
|
||||
"Days": 365
|
||||
},
|
||||
"Filter": {
|
||||
"Prefix": "prowler-reports/"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
<Note>
|
||||
[Note: Screenshot of slide 107 showing S3 integration - to be added]
|
||||
</Note>
|
||||
|
||||
## Step 10: Integrate with Third-Party Tools
|
||||
|
||||
**Send to Slack:**
|
||||
```bash
|
||||
prowler aws --security-hub | \
|
||||
jq -r '.findings[] | select(.status=="FAIL" and .severity=="critical")' | \
|
||||
curl -X POST -H 'Content-type: application/json' \
|
||||
--data @- https://hooks.slack.com/services/YOUR/WEBHOOK/URL
|
||||
```
|
||||
|
||||
**Send to Jira:**
|
||||
Create Jira tickets for critical findings using Jira API:
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
JIRA_URL="https://your-domain.atlassian.net"
|
||||
JIRA_API_TOKEN="your-api-token"
|
||||
JIRA_PROJECT="SEC"
|
||||
|
||||
# Extract critical findings
|
||||
FINDINGS=$(prowler aws -o json-ocsf | \
|
||||
jq '.findings[] | select(.status=="FAIL" and .severity=="critical")')
|
||||
|
||||
# Create Jira tickets
|
||||
echo "$FINDINGS" | jq -c '.' | while read finding; do
|
||||
TITLE=$(echo $finding | jq -r '.check_title')
|
||||
DESCRIPTION=$(echo $finding | jq -r '.status_extended')
|
||||
|
||||
curl -X POST "$JIRA_URL/rest/api/2/issue" \
|
||||
-H "Authorization: Bearer $JIRA_API_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "{
|
||||
\"fields\": {
|
||||
\"project\": {\"key\": \"$JIRA_PROJECT\"},
|
||||
\"summary\": \"$TITLE\",
|
||||
\"description\": \"$DESCRIPTION\",
|
||||
\"issuetype\": {\"name\": \"Task\"}
|
||||
}
|
||||
}"
|
||||
done
|
||||
```
|
||||
|
||||
**Send to Splunk:**
|
||||
```bash
|
||||
prowler aws -o json-ocsf | \
|
||||
curl -k https://splunk-server:8088/services/collector/event \
|
||||
-H "Authorization: Splunk YOUR-HEC-TOKEN" \
|
||||
-d @-
|
||||
```
|
||||
|
||||
## Step 11: Automate Security Hub Updates
|
||||
|
||||
Create a Lambda function to run Prowler periodically:
|
||||
|
||||
**Lambda Function (Python):**
|
||||
```python
|
||||
import subprocess
|
||||
import boto3
|
||||
|
||||
def lambda_handler(event, context):
|
||||
# Run Prowler with Security Hub integration
|
||||
result = subprocess.run(
|
||||
['prowler', 'aws', '--security-hub'],
|
||||
capture_output=True,
|
||||
text=True
|
||||
)
|
||||
|
||||
return {
|
||||
'statusCode': 200,
|
||||
'body': f'Prowler scan completed. Output: {result.stdout}'
|
||||
}
|
||||
```
|
||||
|
||||
**Schedule with EventBridge:**
|
||||
```bash
|
||||
aws events put-rule \
|
||||
--name DailyProwlerScan \
|
||||
--schedule-expression "cron(0 2 * * ? *)"
|
||||
|
||||
aws events put-targets \
|
||||
--rule DailyProwlerScan \
|
||||
--targets "Id"="1","Arn"="arn:aws:lambda:region:account:function:ProwlerScanFunction"
|
||||
```
|
||||
|
||||
<Note>
|
||||
[Note: Screenshot of slide 111 showing automated integration - to be added]
|
||||
</Note>
|
||||
|
||||
## Verification Steps
|
||||
|
||||
Confirm successful lab completion:
|
||||
|
||||
1. AWS Security Hub enabled
|
||||
2. Prowler findings sent to Security Hub
|
||||
3. Findings visible in Security Hub console
|
||||
4. Subsequent scans update existing findings
|
||||
5. S3 integration configured for report storage
|
||||
6. Third-party integration examples tested
|
||||
|
||||
## Expected Outcomes
|
||||
|
||||
After completing this lab, you should:
|
||||
|
||||
* Understand Security Hub integration
|
||||
* Know how to send findings to Security Hub
|
||||
* Be able to configure automated synchronization
|
||||
* Have integrated with S3 for storage
|
||||
* Be familiar with third-party tool integrations
|
||||
|
||||
## Security Hub Benefits
|
||||
|
||||
**Centralized Security:**
|
||||
* Aggregate findings from multiple tools
|
||||
* Unified view across AWS accounts and regions
|
||||
* Compliance dashboard
|
||||
|
||||
**Automated Workflows:**
|
||||
* Trigger remediation workflows
|
||||
* Create incidents automatically
|
||||
* Integrate with SIEM tools
|
||||
|
||||
**Prioritization:**
|
||||
* Filter by severity and compliance status
|
||||
* Track remediation progress
|
||||
* Generate executive reports
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
**Issue:** Security Hub not enabled
|
||||
* **Solution:** Run `aws securityhub enable-security-hub` to enable
|
||||
|
||||
**Issue:** Permission denied sending findings
|
||||
* **Solution:** Ensure IAM role has `securityhub:BatchImportFindings` permission
|
||||
|
||||
**Issue:** Findings not appearing in Security Hub
|
||||
* **Solution:** Check Prowler output for errors, verify region configuration
|
||||
|
||||
**Issue:** Rate limit errors
|
||||
* **Solution:** Prowler batches findings automatically; retry if transient failures occur
|
||||
|
||||
## Next Steps
|
||||
|
||||
Continue to [Lab 8: Prowler SaaS Platform](/workshop/lab-08-prowler-saas) to explore the managed Prowler Cloud platform with advanced features.
|
||||
|
||||
## Additional Resources
|
||||
|
||||
* [Security Hub Integration Guide](/user-guide/providers/aws/securityhub)
|
||||
* [S3 Integration Guide](/user-guide/providers/aws/s3)
|
||||
* [Integrations Documentation](/user-guide/cli/tutorials/integrations)
|
||||
* [AWS Security Hub Documentation](https://docs.aws.amazon.com/securityhub/)
|
||||
@@ -1,440 +0,0 @@
|
||||
---
|
||||
title: "Lab 8: Prowler SaaS Platform"
|
||||
description: "Explore Prowler Cloud's managed platform with advanced features, team collaboration, and continuous monitoring"
|
||||
---
|
||||
|
||||
<Note>
|
||||
**Tags:** `workshop` `prowler-cloud` `saas` `intermediate` `platform` `collaboration`
|
||||
</Note>
|
||||
|
||||
# Lab 8: Prowler SaaS Platform
|
||||
|
||||
Learn to use Prowler Cloud, the managed SaaS platform that provides advanced security monitoring, team collaboration, compliance dashboards, and AI-powered security insights.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
* Completion of previous labs (recommended but not required)
|
||||
* Prowler Cloud account (free trial available)
|
||||
* Cloud provider accounts (AWS, Azure, or GCP)
|
||||
* Basic understanding of Prowler concepts
|
||||
|
||||
**Estimated Time:** 60 minutes
|
||||
|
||||
## Lab Objectives
|
||||
|
||||
By completing this lab, you will:
|
||||
|
||||
* Set up Prowler Cloud account
|
||||
* Connect cloud providers to Prowler Cloud
|
||||
* Navigate the Prowler Cloud interface
|
||||
* Use team collaboration features
|
||||
* Leverage AI-powered security insights
|
||||
* Configure continuous monitoring and alerts
|
||||
* Generate executive compliance reports
|
||||
|
||||
## Step 1: Create Prowler Cloud Account
|
||||
|
||||
Sign up for Prowler Cloud:
|
||||
|
||||
1. Visit [https://cloud.prowler.com](https://cloud.prowler.com)
|
||||
2. Click "Start Free Trial"
|
||||
3. Choose authentication method:
|
||||
* Email/password
|
||||
* Google authentication
|
||||
* GitHub authentication
|
||||
* SSO (for enterprise plans)
|
||||
4. Verify email address
|
||||
5. Complete onboarding wizard
|
||||
|
||||
<Note>
|
||||
[Note: Screenshot of slide 115 showing Prowler Cloud signup - to be added]
|
||||
</Note>
|
||||
|
||||
## Step 2: Connect Your First Cloud Provider
|
||||
|
||||
**Connect AWS Account:**
|
||||
|
||||
1. Navigate to "Providers" in Prowler Cloud
|
||||
2. Click "Add Provider"
|
||||
3. Select "AWS"
|
||||
4. Choose connection method:
|
||||
* **CloudFormation Stack** (recommended)
|
||||
* **Manual IAM Role**
|
||||
5. Deploy CloudFormation template
|
||||
6. Copy Role ARN and External ID
|
||||
7. Test connection
|
||||
8. Click "Save"
|
||||
|
||||
**CloudFormation Stack Deployment:**
|
||||
```bash
|
||||
aws cloudformation create-stack \
|
||||
--stack-name prowler-integration \
|
||||
--template-url https://prowler-public.s3.amazonaws.com/prowler-role.yaml \
|
||||
--parameters ParameterKey=ExternalId,ParameterValue=<your-external-id> \
|
||||
--capabilities CAPABILITY_NAMED_IAM
|
||||
```
|
||||
|
||||
<Note>
|
||||
[Note: Screenshot of slide 118 showing provider connection - to be added]
|
||||
</Note>
|
||||
|
||||
<Tip>
|
||||
The CloudFormation template creates a read-only IAM role with the minimum permissions required for Prowler scans.
|
||||
</Tip>
|
||||
|
||||
## Step 3: Run Your First Cloud Scan
|
||||
|
||||
Initiate a security scan:
|
||||
|
||||
1. Go to "Scans" page
|
||||
2. Click "New Scan"
|
||||
3. Select provider(s) to scan
|
||||
4. Choose scan type:
|
||||
* **Quick Scan:** Essential security checks
|
||||
* **Full Scan:** Comprehensive assessment
|
||||
* **Compliance Scan:** Framework-specific validation
|
||||
5. Click "Start Scan"
|
||||
|
||||
Monitor scan progress:
|
||||
* Real-time progress indicator
|
||||
* Checks completed
|
||||
* Resources discovered
|
||||
* Findings identified
|
||||
|
||||
<Note>
|
||||
[Note: Screenshot of slide 121 showing scan execution - to be added]
|
||||
</Note>
|
||||
|
||||
## Step 4: Explore the Findings Dashboard
|
||||
|
||||
Navigate findings dashboard:
|
||||
|
||||
**Overview Statistics:**
|
||||
* Total findings by severity
|
||||
* Compliance score
|
||||
* Trend over time
|
||||
* Top affected services
|
||||
|
||||
**Filtering Options:**
|
||||
* Severity (Critical, High, Medium, Low)
|
||||
* Status (Open, In Progress, Resolved)
|
||||
* Cloud provider
|
||||
* Service
|
||||
* Compliance framework
|
||||
* Resource tags
|
||||
|
||||
**Finding Details:**
|
||||
* Detailed description
|
||||
* Affected resources
|
||||
* Risk assessment
|
||||
* Remediation steps
|
||||
* Related compliance requirements
|
||||
|
||||
<Note>
|
||||
[Note: Screenshot of slide 124 showing findings dashboard - to be added]
|
||||
</Note>
|
||||
|
||||
## Step 5: Use AI-Powered Security Insights
|
||||
|
||||
Leverage Prowler Lighthouse AI features:
|
||||
|
||||
**AI Security Assistant:**
|
||||
1. Click "Lighthouse" in navigation
|
||||
2. Ask questions about your security posture:
|
||||
* "What are my critical security risks?"
|
||||
* "Show me publicly exposed resources"
|
||||
* "How can I improve my compliance score?"
|
||||
* "What encryption issues exist?"
|
||||
|
||||
**AI Remediation Guidance:**
|
||||
* Select any finding
|
||||
* Click "AI Remediation"
|
||||
* Review generated remediation steps
|
||||
* Get customized code/CLI commands
|
||||
* Apply fixes with confidence
|
||||
|
||||
**AI Threat Analysis:**
|
||||
* Identifies attack patterns
|
||||
* Correlates related findings
|
||||
* Suggests priority order for remediation
|
||||
* Explains security impact
|
||||
|
||||
<Note>
|
||||
[Note: Screenshot of slide 127 showing Lighthouse AI - to be added]
|
||||
</Note>
|
||||
|
||||
## Step 6: Configure Team Collaboration
|
||||
|
||||
Set up team access and workflows:
|
||||
|
||||
**Invite Team Members:**
|
||||
1. Go to "Settings" → "Team"
|
||||
2. Click "Invite Member"
|
||||
3. Enter email address
|
||||
4. Assign role:
|
||||
* **Admin:** Full access
|
||||
* **Editor:** Scan and remediate
|
||||
* **Viewer:** Read-only access
|
||||
5. Send invitation
|
||||
|
||||
**Assign Findings:**
|
||||
1. Select findings
|
||||
2. Click "Assign"
|
||||
3. Choose team member
|
||||
4. Add due date
|
||||
5. Add comments/notes
|
||||
|
||||
**Workflow States:**
|
||||
* Open → New finding
|
||||
* In Progress → Being investigated/fixed
|
||||
* Resolved → Remediated
|
||||
* False Positive → Not applicable
|
||||
* Risk Accepted → Acknowledged but not fixed
|
||||
|
||||
<Note>
|
||||
[Note: Screenshot of slide 130 showing team collaboration - to be added]
|
||||
</Note>
|
||||
|
||||
## Step 7: Configure Continuous Monitoring
|
||||
|
||||
Set up automated scanning:
|
||||
|
||||
**Scheduled Scans:**
|
||||
1. Go to "Scans" → "Schedules"
|
||||
2. Click "Create Schedule"
|
||||
3. Configure:
|
||||
* Name: "Daily Security Scan"
|
||||
* Frequency: Daily, Weekly, or Custom cron
|
||||
* Time: 2:00 AM UTC
|
||||
* Providers: Select all
|
||||
* Notification preferences
|
||||
4. Save schedule
|
||||
|
||||
**Real-Time Monitoring:**
|
||||
* Enable CloudTrail integration
|
||||
* Receive alerts for security events
|
||||
* Detect configuration drift
|
||||
* Identify new resources
|
||||
|
||||
<Tip>
|
||||
Schedule scans during off-peak hours to minimize performance impact on your cloud APIs.
|
||||
</Tip>
|
||||
|
||||
## Step 8: Configure Alerts and Notifications
|
||||
|
||||
Set up security alerts:
|
||||
|
||||
**Alert Rules:**
|
||||
1. Navigate to "Alerts"
|
||||
2. Click "Create Alert Rule"
|
||||
3. Define conditions:
|
||||
* Finding severity ≥ High
|
||||
* Compliance score drops below 80%
|
||||
* New critical findings discovered
|
||||
* Public exposure detected
|
||||
4. Choose notification channels:
|
||||
* Email
|
||||
* Slack
|
||||
* Microsoft Teams
|
||||
* PagerDuty
|
||||
* Webhooks
|
||||
5. Save rule
|
||||
|
||||
**Slack Integration:**
|
||||
1. Go to "Integrations" → "Slack"
|
||||
2. Click "Connect to Slack"
|
||||
3. Authorize Prowler app
|
||||
4. Select channel for notifications
|
||||
5. Configure alert preferences
|
||||
|
||||
<Note>
|
||||
[Note: Screenshot of slide 134 showing alert configuration - to be added]
|
||||
</Note>
|
||||
|
||||
## Step 9: Generate Compliance Reports
|
||||
|
||||
Create compliance reports for auditors:
|
||||
|
||||
**Compliance Dashboard:**
|
||||
1. Navigate to "Compliance"
|
||||
2. View compliance scores by framework:
|
||||
* CIS Benchmarks
|
||||
* PCI-DSS
|
||||
* HIPAA
|
||||
* SOC2
|
||||
* ISO 27001
|
||||
3. Drill down into requirements
|
||||
4. View evidence for each control
|
||||
|
||||
**Export Reports:**
|
||||
1. Select compliance framework
|
||||
2. Click "Generate Report"
|
||||
3. Choose format:
|
||||
* PDF (executive summary)
|
||||
* Excel (detailed findings)
|
||||
* CSV (raw data)
|
||||
4. Schedule recurring reports:
|
||||
* Weekly status updates
|
||||
* Monthly compliance reports
|
||||
* Quarterly audit packages
|
||||
|
||||
**Report Customization:**
|
||||
* Add company logo
|
||||
* Include executive summary
|
||||
* Filter by business unit
|
||||
* Show remediation progress
|
||||
* Include trend analysis
|
||||
|
||||
<Note>
|
||||
[Note: Screenshot of slide 137 showing compliance reports - to be added]
|
||||
</Note>
|
||||
|
||||
## Step 10: Multi-Account and Multi-Cloud Management
|
||||
|
||||
Manage multiple cloud environments:
|
||||
|
||||
**Add Multiple Providers:**
|
||||
1. Connect AWS accounts (dev, staging, production)
|
||||
2. Connect Azure subscriptions
|
||||
3. Connect GCP projects
|
||||
4. Organize with tags/labels
|
||||
|
||||
**Provider Groups:**
|
||||
1. Create provider groups:
|
||||
* Production environments
|
||||
* Development environments
|
||||
* By business unit
|
||||
* By geographic region
|
||||
2. Run group-wide scans
|
||||
3. Generate consolidated reports
|
||||
|
||||
**Cross-Cloud Insights:**
|
||||
* Compare security posture across providers
|
||||
* Identify configuration inconsistencies
|
||||
* Standardize security policies
|
||||
* Track multi-cloud compliance
|
||||
|
||||
<Note>
|
||||
[Note: Screenshot of slide 140 showing multi-cloud management - to be added]
|
||||
</Note>
|
||||
|
||||
## Step 11: Advanced Features
|
||||
|
||||
Explore advanced Prowler Cloud capabilities:
|
||||
|
||||
**Custom Checks:**
|
||||
* Create organization-specific security policies
|
||||
* Define custom compliance requirements
|
||||
* Share with team
|
||||
|
||||
**API Access:**
|
||||
* Programmatic access to findings
|
||||
* Integrate with internal tools
|
||||
* Automate workflows
|
||||
|
||||
**RBAC (Role-Based Access Control):**
|
||||
* Fine-grained permissions
|
||||
* Provider-level access control
|
||||
* Audit logging
|
||||
|
||||
**Security Integrations:**
|
||||
* AWS Security Hub
|
||||
* Jira
|
||||
* ServiceNow
|
||||
* Splunk
|
||||
* Custom webhooks
|
||||
|
||||
## Verification Steps
|
||||
|
||||
Confirm successful lab completion:
|
||||
|
||||
1. Prowler Cloud account created
|
||||
2. Cloud provider(s) connected
|
||||
3. Security scan completed
|
||||
4. Findings dashboard explored
|
||||
5. AI insights leveraged
|
||||
6. Team collaboration configured
|
||||
7. Continuous monitoring set up
|
||||
8. Compliance reports generated
|
||||
|
||||
## Expected Outcomes
|
||||
|
||||
After completing this lab, you should:
|
||||
|
||||
* Understand Prowler Cloud platform capabilities
|
||||
* Be able to connect and scan cloud providers
|
||||
* Know how to use AI-powered insights
|
||||
* Have configured team collaboration
|
||||
* Be able to generate compliance reports
|
||||
* Have set up continuous monitoring
|
||||
|
||||
## Prowler Cloud vs. Prowler CLI
|
||||
|
||||
**Prowler Cloud Advantages:**
|
||||
* Managed infrastructure (no installation)
|
||||
* Web-based interface
|
||||
* Team collaboration features
|
||||
* AI-powered insights (Lighthouse)
|
||||
* Continuous monitoring
|
||||
* Historical trend analysis
|
||||
* Executive dashboards
|
||||
* Built-in integrations
|
||||
* Scheduled scans
|
||||
* Role-based access control
|
||||
|
||||
**Prowler CLI Advantages:**
|
||||
* Self-hosted (on-premises)
|
||||
* No data leaves your environment
|
||||
* Scriptable and automatable
|
||||
* Free and open source
|
||||
* Custom integrations
|
||||
* Offline scanning
|
||||
|
||||
<Tip>
|
||||
Many organizations use both: Prowler CLI for automated CI/CD pipelines and Prowler Cloud for centralized visibility and team collaboration.
|
||||
</Tip>
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
**Issue:** Cannot connect cloud provider
|
||||
* **Solution:** Verify IAM role permissions and trust relationship, check External ID
|
||||
|
||||
**Issue:** Scan fails or times out
|
||||
* **Solution:** Check provider credentials are valid, ensure APIs are not rate-limited
|
||||
|
||||
**Issue:** No findings appearing
|
||||
* **Solution:** Verify scan completed successfully, check filtering settings
|
||||
|
||||
**Issue:** Alert notifications not received
|
||||
* **Solution:** Verify integration configuration, check notification channel settings
|
||||
|
||||
## Workshop Completion
|
||||
|
||||
Congratulations on completing the Prowler Workshop! You have learned:
|
||||
|
||||
* Prowler CLI installation and basic usage
|
||||
* Threat detection techniques
|
||||
* Custom check development
|
||||
* Multi-cloud security (AWS, Azure, GCP)
|
||||
* Compliance automation
|
||||
* Security tool integrations
|
||||
* Prowler Cloud platform capabilities
|
||||
|
||||
## Next Steps
|
||||
|
||||
Continue your Prowler journey:
|
||||
|
||||
* Join the [Prowler Community](https://goto.prowler.com/slack)
|
||||
* Contribute to [Prowler Open Source](https://github.com/prowler-cloud/prowler)
|
||||
* Explore [Prowler Hub](https://hub.prowler.com) for checks and frameworks
|
||||
* Read the [Prowler Documentation](https://docs.prowler.com)
|
||||
* Follow [Prowler on Twitter](https://twitter.com/prowlercloud)
|
||||
* Subscribe to [Prowler YouTube](https://www.youtube.com/@prowlercloud)
|
||||
|
||||
## Additional Resources
|
||||
|
||||
* [Prowler Cloud Documentation](/getting-started/products/prowler-cloud)
|
||||
* [Prowler Cloud Pricing](/getting-started/products/prowler-cloud-pricing)
|
||||
* [AWS Marketplace Listing](/getting-started/products/prowler-cloud-aws-marketplace)
|
||||
* [Prowler API Reference](/getting-started/goto/prowler-api-reference)
|
||||
* [Prowler Lighthouse AI](/user-guide/tutorials/prowler-app-lighthouse)
|
||||
@@ -1,366 +0,0 @@
|
||||
# Prowler SDK Agent Guide
|
||||
|
||||
**Complete guide for AI agents and developers working on the Prowler SDK - the core Python security scanning engine.**
|
||||
|
||||
## Project Overview
|
||||
|
||||
The Prowler SDK is the core Python engine that powers Prowler's cloud security assessment capabilities. It provides:
|
||||
|
||||
- **Multi-cloud Security Scanning**: AWS, Azure, GCP, Kubernetes, GitHub, M365, Oracle Cloud, MongoDB Atlas, and more
|
||||
- **Compliance Frameworks**: 30+ frameworks including CIS, NIST, PCI-DSS, SOC2, GDPR
|
||||
- **1000+ Security Checks**: Comprehensive coverage across all supported providers
|
||||
- **Multiple Output Formats**: JSON, CSV, HTML, ASFF, OCSF, and compliance-specific formats
|
||||
|
||||
## Mission & Scope
|
||||
|
||||
- Maintain and enhance the core Prowler SDK functionality with security and stability as top priorities
|
||||
- Follow best practices for Python patterns, code style, security, and comprehensive testing
|
||||
- To get more information about development guidelines, please refer to the Prowler Developer Guide in `docs/developer-guide/`
|
||||
|
||||
---
|
||||
|
||||
## Architecture Rules
|
||||
|
||||
### 1. Provider Architecture Pattern
|
||||
|
||||
All Prowler providers MUST follow the established pattern:
|
||||
|
||||
```
|
||||
prowler/providers/{provider}/
|
||||
├── {provider}_provider.py # Main provider class
|
||||
├── models.py # Provider-specific models
|
||||
├── config.py # Provider configuration
|
||||
├── exceptions/ # Provider-specific exceptions
|
||||
├── lib/ # Provider libraries (as minimun it should have implemented the next folders: service, arguments, mutelist)
|
||||
│ ├── service/ # Provider-specific service class to be inherited by all services of the provider
|
||||
│ ├── arguments/ # Provider-specific CLI arguments parser
|
||||
│ └── mutelist/ # Provider-specific mutelist functionality
|
||||
└── services/ # All provider services to be audited
|
||||
└── {service}/ # Individual service
|
||||
├── {service}_service.py # Class to fetch the needed resources from the API and store them to be used by the checks
|
||||
├── {service}_client.py # Python instance of the service class to be used by the checks
|
||||
└── {check_name}/ # Individual check folder
|
||||
├── {check_name}.py # Python class to implement the check logic
|
||||
└── {check_name}.metadata.json # JSON file to store the check metadata
|
||||
└── {check_name_2}/ # Other checks can be added to the same service folder
|
||||
├── {check_name_2}.py
|
||||
└── {check_name_2}.metadata.json
|
||||
...
|
||||
└── {service_2}/ # Other services can be added to the same provider folder
|
||||
...
|
||||
```
|
||||
|
||||
### 2. Check Implementation Standards
|
||||
|
||||
Every security check MUST implement:
|
||||
|
||||
```python
|
||||
from prowler.lib.check.models import Check, CheckReport<Provider>
|
||||
from prowler.providers.<provider>.services.<service>.<service>_client import <service>_client
|
||||
|
||||
class check_name(Check):
|
||||
"""Ensure that <resource> meets <security_requirement>."""
|
||||
def execute(self) -> list[CheckReport<Provider>]:
|
||||
"""Execute the check logic.
|
||||
|
||||
Returns:
|
||||
A list of reports containing the result of the check.
|
||||
"""
|
||||
findings = []
|
||||
# Check implementation here
|
||||
for resource in <service>_client.<resources>:
|
||||
# Security validation logic
|
||||
report = CheckReport<Provider>(metadata=self.metadata(), resource=resource)
|
||||
report.status = "PASS" | "FAIL"
|
||||
report.status_extended = "Detailed explanation"
|
||||
findings.append(report) # Add the report to the list of findings
|
||||
return findings
|
||||
```
|
||||
|
||||
### 3. Compliance Framework Integration
|
||||
|
||||
All compliance frameworks must be defined in:
|
||||
- `prowler/compliance/{provider}/{framework}.json`
|
||||
- Follow the established Compliance model structure
|
||||
- Include proper requirement mappings and metadata
|
||||
|
||||
---
|
||||
|
||||
## Tech Stack
|
||||
|
||||
- **Language**: Python 3.9+
|
||||
- **Dependency Management**: Poetry 2+
|
||||
- **CLI Framework**: Custom argument parser with provider-specific subcommands
|
||||
- **Testing**: Pytest with extensive unit and integration tests
|
||||
- **Code Quality**: Pre-commit hooks for Black, Flake8, Pylint, Bandit for security scanning
|
||||
|
||||
## Commands
|
||||
|
||||
### Development Environment
|
||||
|
||||
```bash
|
||||
# Core development setup
|
||||
poetry install --with dev # Install all dependencies
|
||||
poetry run pre-commit install # Install pre-commit hooks
|
||||
|
||||
# Code quality
|
||||
poetry run pre-commit run --all-files
|
||||
|
||||
# Run tests
|
||||
poetry run pytest -n auto -vvv -s -x tests/
|
||||
```
|
||||
|
||||
### Running Prowler CLI
|
||||
|
||||
```bash
|
||||
# Run Prowler
|
||||
poetry run python prowler-cli.py --help
|
||||
|
||||
# Run Prowler with a specific provider
|
||||
poetry run python prowler-cli.py <provider>
|
||||
|
||||
# Run Prowler with error logging
|
||||
poetry run python prowler-cli.py <provider> --log-level ERROR --verbose
|
||||
|
||||
# Run specific checks
|
||||
poetry run python prowler-cli.py <provider> --checks <check_name_1> <check_name_2>
|
||||
```
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
prowler/
|
||||
├── __main__.py # Main CLI entry point
|
||||
├── config/ # Global configuration
|
||||
│ ├── config.py # Core configuration settings
|
||||
│ └── __init__.py
|
||||
├── lib/ # Core library functions
|
||||
│ ├── check/ # Check execution engine
|
||||
│ │ ├── check.py # Check execution logic
|
||||
│ │ ├── checks_loader.py # Dynamic check loading
|
||||
│ │ ├── compliance.py # Compliance framework handling
|
||||
│ │ └── models.py # Check and report models
|
||||
│ ├── cli/ # Command-line interface
|
||||
│ │ └── parser.py # Argument parsing
|
||||
│ ├── outputs/ # Output format handlers
|
||||
│ │ ├── csv/ # CSV output
|
||||
│ │ ├── html/ # HTML reports
|
||||
│ │ ├── json/ # JSON formats
|
||||
│ │ └── compliance/ # Compliance reports
|
||||
│ ├── scan/ # Scan orchestration
|
||||
│ ├── utils/ # Utility functions
|
||||
│ └── mutelist/ # Mute list functionality
|
||||
├── providers/ # Cloud provider implementations
|
||||
│ ├── aws/ # AWS provider
|
||||
│ ├── azure/ # Azure provider
|
||||
│ ├── gcp/ # Google Cloud provider
|
||||
│ ├── kubernetes/ # Kubernetes provider
|
||||
│ ├── github/ # GitHub provider
|
||||
│ ├── m365/ # Microsoft 365 provider
|
||||
│ ├── mongodbatlas/ # MongoDB Atlas provider
|
||||
│ ├── oci/ # Oracle Cloud provider
|
||||
│ ├── ...
|
||||
│ └── common/ # Shared provider utilities
|
||||
├── compliance/ # Compliance framework definitions
|
||||
│ ├── aws/ # AWS compliance frameworks
|
||||
│ ├── azure/ # Azure compliance frameworks
|
||||
│ ├── gcp/ # GCP compliance frameworks
|
||||
│ ├── ...
|
||||
└── exceptions/ # Global exception definitions
|
||||
```
|
||||
|
||||
## Key Components
|
||||
|
||||
### 1. Provider System
|
||||
|
||||
Each cloud provider implements:
|
||||
|
||||
```python
|
||||
class Provider:
|
||||
"""Base provider class"""
|
||||
|
||||
def __init__(self, arguments):
|
||||
self.session = self._setup_session(arguments)
|
||||
self.regions = self._get_regions()
|
||||
# Initialize all services
|
||||
|
||||
def _setup_session(self, arguments):
|
||||
"""Provider-specific authentication"""
|
||||
pass
|
||||
|
||||
def _get_regions(self):
|
||||
"""Get available regions for provider"""
|
||||
pass
|
||||
```
|
||||
|
||||
### 2. Check Engine
|
||||
|
||||
The check execution system:
|
||||
|
||||
- **Dynamic Loading**: Automatically discovers and loads checks
|
||||
- **Parallel Execution**: Runs checks in parallel for performance
|
||||
- **Error Isolation**: Individual check failures don't affect others
|
||||
- **Comprehensive Reporting**: Detailed findings with remediation guidance
|
||||
|
||||
### 3. Compliance Framework Engine
|
||||
|
||||
Compliance frameworks are defined as JSON files mapping checks to requirements:
|
||||
|
||||
```json
|
||||
{
|
||||
"Framework": "CIS",
|
||||
"Name": "CIS Amazon Web Services Foundations Benchmark v2.0.0",
|
||||
"Version": "2.0",
|
||||
"Provider": "AWS",
|
||||
"Description": "The CIS Amazon Web Services Foundations Benchmark provides prescriptive guidance for configuring security options for a subset of Amazon Web Services with an emphasis on foundational, testable, and architecture agnostic settings.",
|
||||
"Requirements": [
|
||||
{
|
||||
"Id": "1.1",
|
||||
"Description": "Maintain current contact details",
|
||||
"Checks": ["account_contact_details_configured"]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### 4. Output System
|
||||
|
||||
Multiple output formats supported:
|
||||
|
||||
- **JSON**: Machine-readable findings
|
||||
- **CSV**: Spreadsheet-compatible format
|
||||
- **HTML**: Interactive web reports
|
||||
- **ASFF**: AWS Security Finding Format
|
||||
- **OCSF**: Open Cybersecurity Schema Framework
|
||||
|
||||
## Development Patterns
|
||||
|
||||
### Adding New Cloud Providers
|
||||
|
||||
1. **Create Provider Structure**:
|
||||
```bash
|
||||
mkdir -p prowler/providers/{provider}
|
||||
mkdir -p prowler/providers/{provider}/services
|
||||
mkdir -p prowler/providers/{provider}/lib/{service,arguments,mutelist}
|
||||
mkdir -p prowler/providers/{provider}/exceptions
|
||||
```
|
||||
|
||||
2. **Implement Provider Class**:
|
||||
```python
|
||||
from prowler.providers.common.provider import Provider
|
||||
|
||||
class NewProvider(Provider):
|
||||
def __init__(self, arguments):
|
||||
super().__init__(arguments)
|
||||
# Provider-specific initialization
|
||||
```
|
||||
|
||||
3. **Add Provider to CLI**:
|
||||
Update `prowler/lib/cli/parser.py` to include new provider arguments.
|
||||
|
||||
### Adding New Security Checks
|
||||
|
||||
The most common high level steps to create a new check are:
|
||||
|
||||
1. Prerequisites:
|
||||
- Verify the check does not already exist by searching in the same service folder as `prowler/providers/<provider>/services/<service>/<check_name_want_to_implement>/`.
|
||||
- Ensure required provider and service exist. If not, you will need to create them first.
|
||||
- Confirm the service has implemented all required methods and attributes for the check (in most cases, you will need to add or modify some methods in the service to get the data you need for the check).
|
||||
2. Navigate to the service directory. The path should be as follows: `prowler/providers/<provider>/services/<service>`.
|
||||
3. Create a check-specific folder. The path should follow this pattern: `prowler/providers/<provider>/services/<service>/<check_name_want_to_implement>`. Adhere to the [Naming Format for Checks](/developer-guide/checks#naming-format-for-checks).
|
||||
4. Create the check files, you can use next commands:
|
||||
```bash
|
||||
mkdir -p prowler/providers/<provider>/services/<service>/<check_name_want_to_implement>
|
||||
touch prowler/providers/<provider>/services/<service>/<check_name_want_to_implement>/__init__.py
|
||||
touch prowler/providers/<provider>/services/<service>/<check_name_want_to_implement>/<check_name_want_to_implement>.py
|
||||
touch prowler/providers/<provider>/services/<service>/<check_name_want_to_implement>/<check_name_want_to_implement>.metadata.json
|
||||
```
|
||||
5. Run the check locally to ensure it works as expected. For checking you can use the CLI in the next way:
|
||||
- To ensure the check has been detected by Prowler: `poetry run python prowler-cli.py <provider> --list-checks | grep <check_name>`.
|
||||
- To run the check, to find possible issues: `poetry run python prowler-cli.py <provider> --log-level ERROR --verbose --check <check_name>`.
|
||||
6. Create comprehensive tests for the check that cover multiple scenarios including both PASS (compliant) and FAIL (non-compliant) cases. For detailed information about test structure and implementation guidelines, refer to the [Testing](/developer-guide/unit-testing) documentation.
|
||||
7. If the check and its corresponding tests are working as expected, you can submit a PR to Prowler.
|
||||
|
||||
### Adding Compliance Frameworks
|
||||
|
||||
1. **Create Framework File**:
|
||||
```bash
|
||||
# Create prowler/compliance/{provider}/{framework}.json
|
||||
```
|
||||
|
||||
2. **Define Requirements**:
|
||||
Map framework requirements to existing checks.
|
||||
|
||||
3. **Test Compliance**:
|
||||
```bash
|
||||
poetry run python -m prowler {provider} --compliance {framework}
|
||||
```
|
||||
|
||||
## Code Quality Standards
|
||||
|
||||
### 1. Python Style
|
||||
|
||||
- **PEP 8 Compliance**: Enforced by black and flake8
|
||||
- **Type Hints**: Required for all public functions
|
||||
- **Docstrings**: Required for all classes and methods
|
||||
- **Import Organization**: Use isort for consistent import ordering
|
||||
|
||||
```python
|
||||
import standard_library
|
||||
|
||||
from third_party import library
|
||||
|
||||
from prowler.lib import internal_module
|
||||
|
||||
class ExampleClass:
|
||||
"""Class docstring."""
|
||||
|
||||
def method(self, param: str) -> dict | list | None:
|
||||
"""Method docstring.
|
||||
|
||||
Args:
|
||||
param: Description of parameter
|
||||
|
||||
Returns:
|
||||
Description of return value
|
||||
"""
|
||||
return None
|
||||
```
|
||||
|
||||
### 2. Error Handling
|
||||
|
||||
```python
|
||||
from prowler.lib.logger import logger
|
||||
|
||||
try:
|
||||
# Risky operation
|
||||
result = api_call()
|
||||
except ProviderSpecificException as e:
|
||||
logger.error(f"Provider error: {e}")
|
||||
# Graceful handling
|
||||
except Exception as e:
|
||||
logger.error(f"Unexpected error: {e}")
|
||||
# Never let checks crash the entire scan
|
||||
```
|
||||
|
||||
### 3. Security Practices
|
||||
|
||||
- **No Hardcoded Secrets**: Use environment variables or secure credential management
|
||||
- **Input Validation**: Validate all external inputs
|
||||
- **Principle of Least Privilege**: Request minimal necessary permissions
|
||||
- **Secure Defaults**: Default to secure configurations
|
||||
|
||||
## Testing Guidelines
|
||||
|
||||
### Unit Tests
|
||||
|
||||
- **100% Coverage Goal**: Aim for complete test coverage
|
||||
- **Mock External Services**: Use mock objects to simulate the external services
|
||||
- **Test Edge Cases**: Include error conditions and boundary cases
|
||||
|
||||
## References
|
||||
|
||||
- **Root Project Guide**: `../AGENTS.md` (takes priority for cross-component guidance)
|
||||
- **Provider Examples**: Reference existing providers for implementation patterns
|
||||
- **Check Examples**: Study existing checks for proper implementation patterns
|
||||
- **Compliance Framework Examples**: Review existing frameworks for structure
|
||||
@@ -2,22 +2,6 @@
|
||||
|
||||
All notable changes to the **Prowler SDK** are documented in this file.
|
||||
|
||||
## [v5.14.0] (Prowler UNRELEASED)
|
||||
|
||||
### Added
|
||||
- GitHub provider check `organization_default_repository_permission_strict` [(#8785)](https://github.com/prowler-cloud/prowler/pull/8785)
|
||||
- Update AWS Direct Connect service metadata to new format [(#8855)](https://github.com/prowler-cloud/prowler/pull/8855)
|
||||
|
||||
---
|
||||
|
||||
## [v5.13.1] (Prowler UNRELEASED)
|
||||
|
||||
### Fixed
|
||||
- Add `resource_name` for checks under `logging` for the GCP provider [(#9023)](https://github.com/prowler-cloud/prowler/pull/9023)
|
||||
|
||||
|
||||
---
|
||||
|
||||
## [v5.13.0] (Prowler v5.13.0)
|
||||
|
||||
### Added
|
||||
|
||||
@@ -113,6 +113,7 @@ from prowler.providers.m365.models import M365OutputOptions
|
||||
from prowler.providers.mongodbatlas.models import MongoDBAtlasOutputOptions
|
||||
from prowler.providers.nhn.models import NHNOutputOptions
|
||||
from prowler.providers.oraclecloud.models import OCIOutputOptions
|
||||
from prowler.providers.alibabacloud.models import AlibabaCloudOutputOptions
|
||||
|
||||
|
||||
def prowler():
|
||||
@@ -336,6 +337,10 @@ def prowler():
|
||||
output_options = OCIOutputOptions(
|
||||
args, bulk_checks_metadata, global_provider.identity
|
||||
)
|
||||
elif provider == "alibabacloud":
|
||||
output_options = AlibabaCloudOutputOptions(
|
||||
args, bulk_checks_metadata, global_provider.identity
|
||||
)
|
||||
|
||||
# Run the quick inventory for the provider if available
|
||||
if hasattr(args, "quick_inventory") and args.quick_inventory:
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
{
|
||||
"Framework": "CIS",
|
||||
"Provider": "alibabacloud",
|
||||
"Version": "1.0",
|
||||
"Description": "CIS Alibaba Cloud Foundations Benchmark",
|
||||
"Requirements": [
|
||||
{
|
||||
"Id": "2.1",
|
||||
"Description": "Ensure that encryption is enabled for ECS disks",
|
||||
"Attributes": [
|
||||
{
|
||||
"Section": "2. Storage",
|
||||
"SubSection": "2.1 ECS Disk Encryption",
|
||||
"Profile": "Level 1",
|
||||
"AssessmentStatus": "Automated",
|
||||
"Description": "ECS disk encryption provides an additional layer of security by encrypting data stored on ECS instances. This helps protect sensitive data from unauthorized access if the physical storage media is compromised or if snapshots are inadvertently shared.",
|
||||
"RationaleStatement": "Encrypting data at rest reduces the risk of unauthorized access to sensitive information stored on ECS disks. Without encryption, data stored on disks is vulnerable if an attacker gains physical access to the storage media or if disk snapshots are exposed.",
|
||||
"ImpactStatement": "Enabling encryption may have a minimal performance impact on disk I/O operations. Additionally, encrypted disks cannot be converted to unencrypted disks, so this decision should be made during the initial disk creation or through migration.",
|
||||
"RemediationProcedure": "To enable encryption on new ECS disks:\n1. Create a KMS key in the Alibaba Cloud KMS console\n2. When creating a new ECS disk, enable the 'Encrypted' option\n3. Select the KMS key to use for encryption\n\nFor existing unencrypted disks:\n1. Create a snapshot of the unencrypted disk\n2. Create a new encrypted disk from the snapshot using a KMS key\n3. Detach the old disk and attach the new encrypted disk\n4. Delete the old unencrypted disk and snapshot",
|
||||
"AuditProcedure": "Using the Prowler CLI: prowler alibabacloud --checks ecs_disk_encryption_enabled\n\nUsing the Alibaba Cloud Console:\n1. Log in to the ECS console\n2. Navigate to Storage & Snapshots > Disks\n3. For each disk, check the 'Encrypted' column\n4. Verify that all disks show 'Yes' for encryption",
|
||||
"AdditionalInformation": "References:\n- Alibaba Cloud ECS Disk Encryption: https://www.alibabacloud.com/help/en/ecs/user-guide/encryption-overview\n- Alibaba Cloud KMS: https://www.alibabacloud.com/help/en/kms/",
|
||||
"References": [
|
||||
"https://www.alibabacloud.com/help/en/ecs/user-guide/encryption-overview"
|
||||
]
|
||||
}
|
||||
],
|
||||
"Checks": [
|
||||
"ecs_disk_encryption_enabled"
|
||||
]
|
||||
},
|
||||
{
|
||||
"Id": "4.1",
|
||||
"Description": "Ensure no security groups allow ingress from 0.0.0.0/0 to port 22",
|
||||
"Attributes": [
|
||||
{
|
||||
"Section": "4. Networking",
|
||||
"SubSection": "4.1 Security Groups",
|
||||
"Profile": "Level 1",
|
||||
"AssessmentStatus": "Automated",
|
||||
"Description": "Security groups should restrict SSH access (port 22) to specific trusted IP addresses rather than allowing access from the entire internet (0.0.0.0/0). Unrestricted SSH access increases the attack surface and risk of unauthorized access.",
|
||||
"RationaleStatement": "Allowing unrestricted SSH access from the internet exposes ECS instances to brute force attacks, unauthorized access attempts, and potential compromise. Attackers routinely scan the internet for open SSH ports to exploit weak credentials or known vulnerabilities.",
|
||||
"ImpactStatement": "Restricting SSH access to specific IP addresses or ranges may require maintaining an allowlist of trusted IPs. Organizations should implement bastion hosts, VPN access, or use Alibaba Cloud's Session Manager for secure remote access without exposing SSH ports to the internet.",
|
||||
"RemediationProcedure": "To restrict SSH access:\n1. Identify the trusted IP addresses or ranges that require SSH access\n2. In the ECS console, navigate to Network & Security > Security Groups\n3. Select the security group\n4. Find the rule allowing 0.0.0.0/0 access to port 22\n5. Modify the rule to specify the trusted IP address or range\n6. Consider implementing a bastion host or VPN for centralized access control",
|
||||
"AuditProcedure": "Using the Prowler CLI: prowler alibabacloud --checks ecs_instance_ssh_access_restricted\n\nUsing the Alibaba Cloud Console:\n1. Log in to the ECS console\n2. Navigate to Network & Security > Security Groups\n3. For each security group, review the inbound rules\n4. Verify that no rule allows access from 0.0.0.0/0 to port 22",
|
||||
"AdditionalInformation": "Best practices:\n- Use bastion hosts or jump servers for SSH access\n- Implement VPN or Direct Connect for secure remote access\n- Consider using Alibaba Cloud Session Manager\n- Enable Multi-Factor Authentication (MFA) for SSH access\n- Regularly review and update security group rules\n\nReferences:\n- Alibaba Cloud Security Groups: https://www.alibabacloud.com/help/en/ecs/user-guide/security-groups-overview",
|
||||
"References": [
|
||||
"https://www.alibabacloud.com/help/en/ecs/user-guide/security-groups-overview"
|
||||
]
|
||||
}
|
||||
],
|
||||
"Checks": [
|
||||
"ecs_instance_ssh_access_restricted"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -753,9 +753,7 @@
|
||||
{
|
||||
"Id": "1.3.8",
|
||||
"Description": "Base permissions define the permission level automatically granted to all organization members. Define strict base access permissions for all of the repositories in the organization, including new ones.",
|
||||
"Checks": [
|
||||
"organization_default_repository_permission_strict"
|
||||
],
|
||||
"Checks": [],
|
||||
"Attributes": [
|
||||
{
|
||||
"Section": "1 Source Code",
|
||||
|
||||
@@ -12,7 +12,7 @@ from prowler.lib.logger import logger
|
||||
|
||||
timestamp = datetime.today()
|
||||
timestamp_utc = datetime.now(timezone.utc).replace(tzinfo=timezone.utc)
|
||||
prowler_version = "5.14.0"
|
||||
prowler_version = "5.13.0"
|
||||
html_logo_url = "https://github.com/prowler-cloud/prowler/"
|
||||
square_logo_img = "https://prowler.com/wp-content/uploads/logo-html.png"
|
||||
aws_logo = "https://user-images.githubusercontent.com/38561120/235953920-3e3fba08-0795-41dc-b480-9bea57db9f2e.png"
|
||||
|
||||
@@ -659,6 +659,33 @@ class Check_Report_Kubernetes(Check_Report):
|
||||
self.namespace = "cluster-wide"
|
||||
|
||||
|
||||
@dataclass
|
||||
class Check_Report_AlibabaCloud(Check_Report):
|
||||
"""Contains the Alibaba Cloud Check's finding information."""
|
||||
|
||||
resource_name: str
|
||||
resource_id: str
|
||||
resource_arn: str
|
||||
region: str
|
||||
account_uid: str
|
||||
|
||||
def __init__(self, metadata: Dict, resource: Any) -> None:
|
||||
"""Initialize the Alibaba Cloud Check's finding information.
|
||||
|
||||
Args:
|
||||
metadata: The metadata of the check.
|
||||
resource: Basic information about the resource.
|
||||
"""
|
||||
super().__init__(metadata, resource)
|
||||
self.resource_id = (
|
||||
getattr(resource, "id", None) or getattr(resource, "name", None) or ""
|
||||
)
|
||||
self.resource_name = getattr(resource, "name", "")
|
||||
self.resource_arn = getattr(resource, "arn", "")
|
||||
self.region = getattr(resource, "region", "")
|
||||
self.account_uid = ""
|
||||
|
||||
|
||||
@dataclass
|
||||
class CheckReportGithub(Check_Report):
|
||||
"""Contains the GitHub Check's finding information."""
|
||||
|
||||
@@ -26,7 +26,7 @@ def recover_checks_from_provider(
|
||||
# We need to exclude common shared libraries in services
|
||||
if (
|
||||
check_module_name.count(".") == 6
|
||||
and "lib" not in check_module_name
|
||||
and ".lib." not in check_module_name # Exclude .lib. directories, not "lib" substring
|
||||
and (not check_module_name.endswith("_fixer") or include_fixers)
|
||||
):
|
||||
check_path = module_name.module_finder.path
|
||||
|
||||
@@ -337,6 +337,26 @@ class Finding(BaseModel):
|
||||
output_data["resource_uid"] = check_output.resource_id
|
||||
output_data["region"] = check_output.region
|
||||
|
||||
elif provider.type == "alibabacloud":
|
||||
output_data["auth_method"] = (
|
||||
"STS Token"
|
||||
if get_nested_attribute(
|
||||
provider, "session.credentials.security_token"
|
||||
)
|
||||
else "AccessKey"
|
||||
)
|
||||
output_data["account_uid"] = get_nested_attribute(
|
||||
provider, "identity.account_id"
|
||||
)
|
||||
# Use account_name if available, otherwise use account_id
|
||||
account_name = get_nested_attribute(provider, "identity.account_name")
|
||||
if not account_name:
|
||||
account_name = get_nested_attribute(provider, "identity.account_id")
|
||||
output_data["account_name"] = account_name
|
||||
output_data["resource_name"] = check_output.resource_name
|
||||
output_data["resource_uid"] = check_output.resource_arn
|
||||
output_data["region"] = check_output.region
|
||||
|
||||
# check_output Unique ID
|
||||
# TODO: move this to a function
|
||||
# TODO: in Azure, GCP and K8s there are findings without resource_name
|
||||
|
||||
@@ -1020,6 +1020,70 @@ class HTML(Output):
|
||||
)
|
||||
return ""
|
||||
|
||||
@staticmethod
|
||||
def get_alibabacloud_assessment_summary(provider: Provider) -> str:
|
||||
"""
|
||||
get_alibabacloud_assessment_summary gets the HTML assessment summary for the Alibaba Cloud provider
|
||||
|
||||
Args:
|
||||
provider (Provider): the Alibaba Cloud provider object
|
||||
|
||||
Returns:
|
||||
str: the HTML assessment summary
|
||||
"""
|
||||
try:
|
||||
# Get audited regions from provider, not identity
|
||||
if hasattr(provider, "audited_regions") and provider.audited_regions:
|
||||
if isinstance(provider.audited_regions, list):
|
||||
audited_regions = ", ".join(provider.audited_regions)
|
||||
else:
|
||||
audited_regions = str(provider.audited_regions)
|
||||
else:
|
||||
audited_regions = "All Regions"
|
||||
|
||||
auth_method = (
|
||||
"STS Token"
|
||||
if provider.session.credentials.security_token
|
||||
else "AccessKey"
|
||||
)
|
||||
|
||||
return f"""
|
||||
<div class="col-md-2">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
Alibaba Cloud Assessment Summary
|
||||
</div>
|
||||
<ul class="list-group list-group-flush">
|
||||
<li class="list-group-item">
|
||||
<b>Account ID:</b> {provider.identity.account_id}
|
||||
</li>
|
||||
<li class="list-group-item">
|
||||
<b>Audited Regions:</b> {audited_regions}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
Alibaba Cloud Credentials
|
||||
</div>
|
||||
<ul class="list-group list-group-flush">
|
||||
<li class="list-group-item">
|
||||
<b>Authentication Method:</b> {auth_method}
|
||||
</li>
|
||||
<li class="list-group-item">
|
||||
<b>Account ARN:</b> {provider.identity.account_arn}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>"""
|
||||
except Exception as error:
|
||||
logger.error(
|
||||
f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}] -- {error}"
|
||||
)
|
||||
return ""
|
||||
|
||||
@staticmethod
|
||||
def get_assessment_summary(provider: Provider) -> str:
|
||||
"""
|
||||
|
||||
@@ -74,6 +74,9 @@ def display_summary_table(
|
||||
if provider.identity.tenancy_name != "unknown"
|
||||
else provider.identity.tenancy_id
|
||||
)
|
||||
elif provider.type == "alibabacloud":
|
||||
entity_type = "Account"
|
||||
audited_entities = provider.identity.account_id
|
||||
|
||||
# Check if there are findings and that they are not all MANUAL
|
||||
if findings and not all(finding.status == "MANUAL" for finding in findings):
|
||||
|
||||
@@ -0,0 +1,506 @@
|
||||
"""
|
||||
Alibaba Cloud Provider
|
||||
|
||||
This module implements the Alibaba Cloud provider for Prowler security auditing.
|
||||
"""
|
||||
|
||||
import sys
|
||||
from typing import Optional
|
||||
|
||||
from colorama import Fore, Style
|
||||
|
||||
from prowler.config.config import (
|
||||
get_default_mute_file_path,
|
||||
load_and_validate_config_file,
|
||||
)
|
||||
from prowler.lib.logger import logger
|
||||
from prowler.lib.utils.utils import print_boxes
|
||||
from prowler.providers.alibabacloud.config import (
|
||||
ALIBABACLOUD_REGIONS,
|
||||
ALIBABACLOUD_RAM_SESSION_NAME,
|
||||
)
|
||||
from prowler.providers.alibabacloud.exceptions.exceptions import (
|
||||
AlibabaCloudAccountNotFoundError,
|
||||
AlibabaCloudAssumeRoleError,
|
||||
AlibabaCloudAuthenticationError,
|
||||
AlibabaCloudNoCredentialsError,
|
||||
AlibabaCloudSetUpSessionError,
|
||||
)
|
||||
from prowler.providers.alibabacloud.lib.mutelist.mutelist import AlibabaCloudMutelist
|
||||
from prowler.providers.alibabacloud.models import (
|
||||
AlibabaCloudAssumeRoleInfo,
|
||||
AlibabaCloudCredentials,
|
||||
AlibabaCloudIdentityInfo,
|
||||
AlibabaCloudSession,
|
||||
)
|
||||
from prowler.providers.common.models import Audit_Metadata, Connection
|
||||
from prowler.providers.common.provider import Provider
|
||||
|
||||
|
||||
class AlibabacloudProvider(Provider):
|
||||
"""
|
||||
AlibabacloudProvider class implements the Alibaba Cloud provider for Prowler
|
||||
|
||||
This class handles:
|
||||
- Authentication with Alibaba Cloud using AccessKey credentials or STS tokens
|
||||
- RAM role assumption for cross-account auditing
|
||||
- Region management and filtering
|
||||
- Resource discovery and auditing
|
||||
- Mutelist management for finding suppression
|
||||
|
||||
Attributes:
|
||||
_type: Provider type identifier ("alibabacloud")
|
||||
_identity: Alibaba Cloud account identity information
|
||||
_session: Alibaba Cloud session with credentials
|
||||
_regions: List of regions to audit
|
||||
_mutelist: Mutelist for finding suppression
|
||||
_audit_config: Audit configuration dictionary
|
||||
_fixer_config: Fixer configuration dictionary
|
||||
audit_metadata: Audit execution metadata
|
||||
"""
|
||||
|
||||
_type: str = "alibabacloud"
|
||||
_identity: AlibabaCloudIdentityInfo
|
||||
_session: AlibabaCloudSession
|
||||
_regions: list = []
|
||||
_mutelist: AlibabaCloudMutelist
|
||||
_audit_config: dict
|
||||
_fixer_config: dict
|
||||
audit_metadata: Audit_Metadata
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
access_key_id: str = None,
|
||||
access_key_secret: str = None,
|
||||
security_token: str = None,
|
||||
region_ids: list = None,
|
||||
filter_regions: list = None,
|
||||
ram_role_arn: str = None,
|
||||
ram_session_name: str = None,
|
||||
ram_session_duration: int = 3600,
|
||||
ram_external_id: str = None,
|
||||
config_path: str = None,
|
||||
config_content: dict = None,
|
||||
fixer_config: dict = {},
|
||||
mutelist_path: str = None,
|
||||
mutelist_content: dict = None,
|
||||
resource_tags: list[str] = [],
|
||||
resource_ids: list[str] = [],
|
||||
):
|
||||
"""
|
||||
Initialize Alibaba Cloud provider
|
||||
|
||||
Args:
|
||||
access_key_id: Alibaba Cloud AccessKey ID
|
||||
access_key_secret: Alibaba Cloud AccessKey Secret
|
||||
security_token: STS security token for temporary credentials
|
||||
region_ids: List of region IDs to audit
|
||||
filter_regions: List of region IDs to exclude from audit
|
||||
ram_role_arn: RAM role ARN to assume
|
||||
ram_session_name: Session name for RAM role assumption
|
||||
ram_session_duration: Session duration in seconds (900-43200)
|
||||
ram_external_id: External ID for RAM role assumption
|
||||
config_path: Path to audit configuration file
|
||||
config_content: Configuration content dictionary
|
||||
fixer_config: Fixer configuration dictionary
|
||||
mutelist_path: Path to mutelist file
|
||||
mutelist_content: Mutelist content dictionary
|
||||
resource_tags: List of resource tags to filter (key=value format)
|
||||
resource_ids: List of specific resource IDs to audit
|
||||
"""
|
||||
logger.info("Initializing Alibaba Cloud provider...")
|
||||
|
||||
# Validate and load configuration
|
||||
self._audit_config = load_and_validate_config_file(
|
||||
self._type, config_path
|
||||
)
|
||||
if self._audit_config is None:
|
||||
self._audit_config = {}
|
||||
|
||||
# Override with config_content if provided
|
||||
if config_content:
|
||||
self._audit_config.update(config_content)
|
||||
|
||||
self._fixer_config = fixer_config
|
||||
|
||||
# Setup session and authenticate
|
||||
try:
|
||||
self._session = self.setup_session(
|
||||
access_key_id,
|
||||
access_key_secret,
|
||||
security_token,
|
||||
ram_role_arn,
|
||||
ram_session_name or ALIBABACLOUD_RAM_SESSION_NAME,
|
||||
ram_session_duration,
|
||||
ram_external_id,
|
||||
)
|
||||
except Exception as error:
|
||||
logger.critical(f"Failed to set up Alibaba Cloud session: {error}")
|
||||
raise AlibabaCloudSetUpSessionError(str(error))
|
||||
|
||||
# Get account identity
|
||||
try:
|
||||
self._identity = self._set_identity()
|
||||
except Exception as error:
|
||||
logger.critical(
|
||||
f"Failed to retrieve Alibaba Cloud account identity: {error}"
|
||||
)
|
||||
raise AlibabaCloudAccountNotFoundError(str(error))
|
||||
|
||||
# Setup regions
|
||||
self._regions = self._setup_regions(region_ids, filter_regions)
|
||||
|
||||
# Setup mutelist
|
||||
self._mutelist = self._setup_mutelist(mutelist_path, mutelist_content)
|
||||
|
||||
# Set audit metadata
|
||||
self.audit_metadata = Audit_Metadata(
|
||||
services_scanned=0,
|
||||
expected_checks=[],
|
||||
completed_checks=0,
|
||||
audit_progress=0,
|
||||
)
|
||||
|
||||
# Set as global provider
|
||||
Provider.set_global_provider(self)
|
||||
|
||||
logger.info(
|
||||
f"Alibaba Cloud provider initialized for account {self._identity.account_id}"
|
||||
)
|
||||
|
||||
@property
|
||||
def type(self) -> str:
|
||||
"""Return provider type"""
|
||||
return self._type
|
||||
|
||||
@property
|
||||
def identity(self) -> AlibabaCloudIdentityInfo:
|
||||
"""Return provider identity"""
|
||||
return self._identity
|
||||
|
||||
@property
|
||||
def session(self) -> AlibabaCloudSession:
|
||||
"""Return provider session"""
|
||||
return self._session
|
||||
|
||||
@property
|
||||
def audit_config(self) -> dict:
|
||||
"""Return audit configuration"""
|
||||
return self._audit_config
|
||||
|
||||
@property
|
||||
def fixer_config(self) -> dict:
|
||||
"""Return fixer configuration"""
|
||||
return self._fixer_config
|
||||
|
||||
@property
|
||||
def mutelist(self) -> AlibabaCloudMutelist:
|
||||
"""Return mutelist"""
|
||||
return self._mutelist
|
||||
|
||||
def setup_session(
|
||||
self,
|
||||
access_key_id: str,
|
||||
access_key_secret: str,
|
||||
security_token: str = None,
|
||||
ram_role_arn: str = None,
|
||||
ram_session_name: str = ALIBABACLOUD_RAM_SESSION_NAME,
|
||||
ram_session_duration: int = 3600,
|
||||
ram_external_id: str = None,
|
||||
) -> AlibabaCloudSession:
|
||||
"""
|
||||
Setup Alibaba Cloud session with authentication
|
||||
|
||||
Args:
|
||||
access_key_id: AccessKey ID
|
||||
access_key_secret: AccessKey Secret
|
||||
security_token: STS security token (optional)
|
||||
ram_role_arn: RAM role to assume (optional)
|
||||
ram_session_name: Session name for role assumption
|
||||
ram_session_duration: Session duration in seconds
|
||||
ram_external_id: External ID for role assumption
|
||||
|
||||
Returns:
|
||||
AlibabaCloudSession: Configured session object
|
||||
|
||||
Raises:
|
||||
AlibabaCloudNoCredentialsError: If credentials are missing
|
||||
AlibabaCloudAuthenticationError: If authentication fails
|
||||
AlibabaCloudAssumeRoleError: If role assumption fails
|
||||
"""
|
||||
# Validate credentials
|
||||
if not access_key_id or not access_key_secret:
|
||||
logger.critical("Alibaba Cloud credentials are required")
|
||||
raise AlibabaCloudNoCredentialsError()
|
||||
|
||||
try:
|
||||
# Create credentials object
|
||||
credentials = AlibabaCloudCredentials(
|
||||
access_key_id=access_key_id,
|
||||
access_key_secret=access_key_secret,
|
||||
security_token=security_token,
|
||||
)
|
||||
|
||||
# If RAM role is specified, assume the role
|
||||
if ram_role_arn:
|
||||
logger.info(f"Assuming RAM role: {ram_role_arn}")
|
||||
credentials = self._assume_role(
|
||||
credentials,
|
||||
ram_role_arn,
|
||||
ram_session_name,
|
||||
ram_session_duration,
|
||||
ram_external_id,
|
||||
)
|
||||
|
||||
# Create session
|
||||
session = AlibabaCloudSession(
|
||||
credentials=credentials,
|
||||
region_id="cn-hangzhou", # Default region for global APIs
|
||||
)
|
||||
|
||||
logger.info("Alibaba Cloud session established successfully")
|
||||
return session
|
||||
|
||||
except Exception as error:
|
||||
logger.critical(f"Authentication failed: {error}")
|
||||
raise AlibabaCloudAuthenticationError(str(error))
|
||||
|
||||
def _assume_role(
|
||||
self,
|
||||
credentials: AlibabaCloudCredentials,
|
||||
role_arn: str,
|
||||
session_name: str,
|
||||
session_duration: int,
|
||||
external_id: str = None,
|
||||
) -> AlibabaCloudCredentials:
|
||||
"""
|
||||
Assume a RAM role and return temporary credentials
|
||||
|
||||
Args:
|
||||
credentials: Current credentials
|
||||
role_arn: RAM role ARN to assume
|
||||
session_name: Session name
|
||||
session_duration: Session duration in seconds
|
||||
external_id: External ID (optional)
|
||||
|
||||
Returns:
|
||||
AlibabaCloudCredentials: Temporary credentials from STS
|
||||
|
||||
Raises:
|
||||
AlibabaCloudAssumeRoleError: If role assumption fails
|
||||
"""
|
||||
try:
|
||||
# Note: In a real implementation, this would use Alibaba Cloud STS SDK
|
||||
# to call AssumeRole API and get temporary credentials
|
||||
# For now, we'll return the original credentials as a placeholder
|
||||
|
||||
logger.warning(
|
||||
"RAM role assumption not yet fully implemented - using provided credentials"
|
||||
)
|
||||
|
||||
# TODO: Implement actual STS AssumeRole call
|
||||
# from alibabacloud_sts20150401.client import Client as StsClient
|
||||
# from alibabacloud_sts20150401.models import AssumeRoleRequest
|
||||
#
|
||||
# sts_client = StsClient(config)
|
||||
# request = AssumeRoleRequest(
|
||||
# role_arn=role_arn,
|
||||
# role_session_name=session_name,
|
||||
# duration_seconds=session_duration,
|
||||
# external_id=external_id
|
||||
# )
|
||||
# response = sts_client.assume_role(request)
|
||||
#
|
||||
# return AlibabaCloudCredentials(
|
||||
# access_key_id=response.body.credentials.access_key_id,
|
||||
# access_key_secret=response.body.credentials.access_key_secret,
|
||||
# security_token=response.body.credentials.security_token,
|
||||
# expiration=response.body.credentials.expiration,
|
||||
# )
|
||||
|
||||
return credentials
|
||||
|
||||
except Exception as error:
|
||||
logger.critical(f"Failed to assume RAM role {role_arn}: {error}")
|
||||
raise AlibabaCloudAssumeRoleError(role_arn, str(error))
|
||||
|
||||
def _set_identity(self) -> AlibabaCloudIdentityInfo:
|
||||
"""
|
||||
Retrieve Alibaba Cloud account identity information
|
||||
|
||||
Returns:
|
||||
AlibabaCloudIdentityInfo: Account identity details
|
||||
|
||||
Raises:
|
||||
AlibabaCloudAccountNotFoundError: If identity cannot be retrieved
|
||||
"""
|
||||
try:
|
||||
logger.info("Retrieving Alibaba Cloud account identity...")
|
||||
|
||||
# Derive account ID from AccessKey ID
|
||||
# Alibaba Cloud AccessKey IDs follow the format: LTAI{account_id_hash}...
|
||||
# For a more accurate implementation, you would call STS GetCallerIdentity API
|
||||
# For now, we'll use the AccessKey ID as a unique identifier
|
||||
|
||||
access_key_id = self._session.credentials.access_key_id
|
||||
|
||||
# Simple implementation: Use the first 12 characters of AccessKey ID as account identifier
|
||||
# In production, you should call:
|
||||
# from alibabacloud_sts20150401.client import Client as StsClient
|
||||
# sts_client = StsClient(config)
|
||||
# response = sts_client.get_caller_identity()
|
||||
# account_id = response.body.account_id
|
||||
|
||||
# For now, create a unique identifier from the AccessKey
|
||||
account_id = access_key_id[:20] if access_key_id else "unknown"
|
||||
account_arn = f"acs:ram::{account_id}:root"
|
||||
|
||||
identity = AlibabaCloudIdentityInfo(
|
||||
account_id=account_id,
|
||||
account_arn=account_arn,
|
||||
)
|
||||
|
||||
logger.info(f"Account ID: {identity.account_id}")
|
||||
return identity
|
||||
|
||||
except Exception as error:
|
||||
logger.critical(f"Failed to get account identity: {error}")
|
||||
raise AlibabaCloudAccountNotFoundError(str(error))
|
||||
|
||||
def _setup_regions(
|
||||
self, region_ids: list = None, filter_regions: list = None
|
||||
) -> list:
|
||||
"""
|
||||
Setup regions to audit
|
||||
|
||||
Args:
|
||||
region_ids: Specific regions to audit (None = all regions)
|
||||
filter_regions: Regions to exclude from audit
|
||||
|
||||
Returns:
|
||||
list: Final list of regions to audit
|
||||
"""
|
||||
# Start with specified regions or all regions
|
||||
if region_ids:
|
||||
regions = [r for r in region_ids if r in ALIBABACLOUD_REGIONS]
|
||||
logger.info(f"Auditing specified regions: {', '.join(regions)}")
|
||||
else:
|
||||
regions = ALIBABACLOUD_REGIONS.copy()
|
||||
logger.info("Auditing all Alibaba Cloud regions")
|
||||
|
||||
# Apply filters
|
||||
if filter_regions:
|
||||
regions = [r for r in regions if r not in filter_regions]
|
||||
logger.info(f"Excluded regions: {', '.join(filter_regions)}")
|
||||
|
||||
logger.info(f"Total regions to audit: {len(regions)}")
|
||||
return regions
|
||||
|
||||
def _setup_mutelist(
|
||||
self, mutelist_path: str = None, mutelist_content: dict = None
|
||||
) -> AlibabaCloudMutelist:
|
||||
"""
|
||||
Setup mutelist for finding suppression
|
||||
|
||||
Args:
|
||||
mutelist_path: Path to mutelist file
|
||||
mutelist_content: Mutelist content dictionary
|
||||
|
||||
Returns:
|
||||
AlibabaCloudMutelist: Configured mutelist instance
|
||||
"""
|
||||
try:
|
||||
# Use default path if not provided
|
||||
if not mutelist_path and not mutelist_content:
|
||||
mutelist_path = get_default_mute_file_path(self._type)
|
||||
|
||||
mutelist = AlibabaCloudMutelist(
|
||||
mutelist_path=mutelist_path,
|
||||
mutelist_content=mutelist_content,
|
||||
provider=self._type,
|
||||
identity=self._identity,
|
||||
)
|
||||
|
||||
logger.info("Mutelist loaded successfully")
|
||||
return mutelist
|
||||
|
||||
except Exception as error:
|
||||
logger.warning(f"Error loading mutelist: {error}")
|
||||
# Return empty mutelist on error
|
||||
return AlibabaCloudMutelist()
|
||||
|
||||
def print_credentials(self) -> None:
|
||||
"""
|
||||
Display Alibaba Cloud credentials and configuration in CLI
|
||||
|
||||
This method prints the current provider configuration including:
|
||||
- Account ID
|
||||
- Regions being audited
|
||||
- Authentication method
|
||||
"""
|
||||
# Account information
|
||||
report_lines = []
|
||||
report_lines.append(
|
||||
f"{Fore.CYAN}Account ID:{Style.RESET_ALL} {self._identity.account_id}"
|
||||
)
|
||||
|
||||
if self._identity.user_name:
|
||||
report_lines.append(
|
||||
f"{Fore.CYAN}User:{Style.RESET_ALL} {self._identity.user_name}"
|
||||
)
|
||||
|
||||
# Regions
|
||||
region_count = len(self._regions)
|
||||
regions_display = (
|
||||
", ".join(self._regions[:5])
|
||||
+ (f" ... (+{region_count - 5} more)" if region_count > 5 else "")
|
||||
)
|
||||
report_lines.append(
|
||||
f"{Fore.CYAN}Regions ({region_count}):{Style.RESET_ALL} {regions_display}"
|
||||
)
|
||||
|
||||
# Authentication method
|
||||
auth_method = (
|
||||
"STS Token" if self._session.credentials.security_token else "AccessKey"
|
||||
)
|
||||
report_lines.append(
|
||||
f"{Fore.CYAN}Authentication:{Style.RESET_ALL} {auth_method}"
|
||||
)
|
||||
|
||||
# Print formatted box
|
||||
print_boxes(report_lines, "Alibaba Cloud Provider Configuration")
|
||||
|
||||
def test_connection(self) -> Connection:
|
||||
"""
|
||||
Test connection to Alibaba Cloud
|
||||
|
||||
Returns:
|
||||
Connection: Connection test result with status and error (if any)
|
||||
"""
|
||||
try:
|
||||
logger.info("Testing connection to Alibaba Cloud...")
|
||||
|
||||
# TODO: Implement actual connection test with a simple API call
|
||||
# For example, call DescribeRegions or GetCallerIdentity
|
||||
# from alibabacloud_ecs20140526.client import Client as EcsClient
|
||||
#
|
||||
# ecs_client = EcsClient(config)
|
||||
# ecs_client.describe_regions()
|
||||
|
||||
logger.info("Connection test successful")
|
||||
return Connection(is_connected=True)
|
||||
|
||||
except Exception as error:
|
||||
logger.error(f"Connection test failed: {error}")
|
||||
return Connection(is_connected=False, error=str(error))
|
||||
|
||||
def validate_arguments(self) -> None:
|
||||
"""
|
||||
Validate provider arguments
|
||||
|
||||
This method is called after initialization to ensure all arguments
|
||||
and configurations are valid.
|
||||
"""
|
||||
# Validation is handled in the CLI arguments parser
|
||||
# This method can be used for additional runtime validations
|
||||
pass
|
||||
@@ -0,0 +1,45 @@
|
||||
"""
|
||||
Alibaba Cloud Provider Configuration
|
||||
|
||||
This module contains configuration constants for the Alibaba Cloud provider.
|
||||
"""
|
||||
|
||||
# Default Alibaba Cloud regions
|
||||
ALIBABACLOUD_REGIONS = [
|
||||
"cn-hangzhou", # China (Hangzhou)
|
||||
"cn-shanghai", # China (Shanghai)
|
||||
"cn-qingdao", # China (Qingdao)
|
||||
"cn-beijing", # China (Beijing)
|
||||
"cn-zhangjiakou", # China (Zhangjiakou)
|
||||
"cn-huhehaote", # China (Hohhot)
|
||||
"cn-wulanchabu", # China (Ulanqab)
|
||||
"cn-shenzhen", # China (Shenzhen)
|
||||
"cn-heyuan", # China (Heyuan)
|
||||
"cn-guangzhou", # China (Guangzhou)
|
||||
"cn-chengdu", # China (Chengdu)
|
||||
"cn-hongkong", # China (Hong Kong)
|
||||
"ap-northeast-1", # Japan (Tokyo)
|
||||
"ap-southeast-1", # Singapore
|
||||
"ap-southeast-2", # Australia (Sydney)
|
||||
"ap-southeast-3", # Malaysia (Kuala Lumpur)
|
||||
"ap-southeast-5", # Indonesia (Jakarta)
|
||||
"ap-southeast-6", # Philippines (Manila)
|
||||
"ap-southeast-7", # Thailand (Bangkok)
|
||||
"ap-south-1", # India (Mumbai)
|
||||
"us-west-1", # US (Silicon Valley)
|
||||
"us-east-1", # US (Virginia)
|
||||
"eu-west-1", # UK (London)
|
||||
"eu-central-1", # Germany (Frankfurt)
|
||||
"me-east-1", # UAE (Dubai)
|
||||
]
|
||||
|
||||
# Alibaba Cloud SDK configuration
|
||||
ALIBABACLOUD_SDK_USER_AGENT = "Prowler"
|
||||
ALIBABACLOUD_SDK_MAX_RETRIES = 3
|
||||
ALIBABACLOUD_SDK_TIMEOUT = 30 # seconds
|
||||
|
||||
# Alibaba Cloud ARN format
|
||||
ALIBABACLOUD_ARN_FORMAT = "acs:{service}:{region}:{account_id}:{resource}"
|
||||
|
||||
# Default RAM role session name
|
||||
ALIBABACLOUD_RAM_SESSION_NAME = "ProwlerSession"
|
||||
@@ -0,0 +1,90 @@
|
||||
"""
|
||||
Alibaba Cloud Provider Exceptions
|
||||
|
||||
This module contains exception classes for the Alibaba Cloud provider.
|
||||
"""
|
||||
|
||||
|
||||
class AlibabaCloudException(Exception):
|
||||
"""Base exception for Alibaba Cloud provider errors"""
|
||||
pass
|
||||
|
||||
|
||||
class AlibabaCloudAuthenticationError(AlibabaCloudException):
|
||||
"""
|
||||
AlibabaCloudAuthenticationError is raised when authentication fails
|
||||
|
||||
This can occur due to:
|
||||
- Invalid AccessKey credentials
|
||||
- Expired STS tokens
|
||||
- Insufficient permissions
|
||||
"""
|
||||
def __init__(self, message="Authentication to Alibaba Cloud failed"):
|
||||
super().__init__(message)
|
||||
|
||||
|
||||
class AlibabaCloudSetUpSessionError(AlibabaCloudException):
|
||||
"""
|
||||
AlibabaCloudSetUpSessionError is raised when session setup fails
|
||||
"""
|
||||
def __init__(self, message="Failed to set up Alibaba Cloud session"):
|
||||
super().__init__(message)
|
||||
|
||||
|
||||
class AlibabaCloudAPIError(AlibabaCloudException):
|
||||
"""
|
||||
AlibabaCloudAPIError is raised when an API call fails
|
||||
"""
|
||||
def __init__(self, service: str, operation: str, error: str):
|
||||
message = f"Alibaba Cloud API Error - Service: {service}, Operation: {operation}, Error: {error}"
|
||||
super().__init__(message)
|
||||
|
||||
|
||||
class AlibabaCloudNoCredentialsError(AlibabaCloudException):
|
||||
"""
|
||||
AlibabaCloudNoCredentialsError is raised when no credentials are found
|
||||
"""
|
||||
def __init__(self, message="No Alibaba Cloud credentials found"):
|
||||
super().__init__(message)
|
||||
|
||||
|
||||
class AlibabaCloudInvalidRegionError(AlibabaCloudException):
|
||||
"""
|
||||
AlibabaCloudInvalidRegionError is raised when an invalid region is specified
|
||||
"""
|
||||
def __init__(self, region: str):
|
||||
message = f"Invalid Alibaba Cloud region: {region}"
|
||||
super().__init__(message)
|
||||
|
||||
|
||||
class AlibabaCloudAssumeRoleError(AlibabaCloudException):
|
||||
"""
|
||||
AlibabaCloudAssumeRoleError is raised when assuming a RAM role fails
|
||||
"""
|
||||
def __init__(self, role_arn: str, error: str):
|
||||
message = f"Failed to assume RAM role {role_arn}: {error}"
|
||||
super().__init__(message)
|
||||
|
||||
|
||||
class AlibabaCloudInvalidAccessKeyError(AlibabaCloudException):
|
||||
"""
|
||||
AlibabaCloudInvalidAccessKeyError is raised when AccessKey credentials are invalid
|
||||
"""
|
||||
def __init__(self, message="Invalid Alibaba Cloud AccessKey credentials"):
|
||||
super().__init__(message)
|
||||
|
||||
|
||||
class AlibabaCloudAccountNotFoundError(AlibabaCloudException):
|
||||
"""
|
||||
AlibabaCloudAccountNotFoundError is raised when account information cannot be retrieved
|
||||
"""
|
||||
def __init__(self, message="Unable to retrieve Alibaba Cloud account information"):
|
||||
super().__init__(message)
|
||||
|
||||
|
||||
class AlibabaCloudConfigValidationError(AlibabaCloudException):
|
||||
"""
|
||||
AlibabaCloudConfigValidationError is raised when configuration validation fails
|
||||
"""
|
||||
def __init__(self, message="Alibaba Cloud configuration validation failed"):
|
||||
super().__init__(message)
|
||||
@@ -0,0 +1,203 @@
|
||||
"""
|
||||
Alibaba Cloud Provider CLI Arguments
|
||||
|
||||
This module defines the command-line interface arguments for the Alibaba Cloud provider.
|
||||
"""
|
||||
|
||||
import os
|
||||
from argparse import ArgumentTypeError, Namespace
|
||||
|
||||
from prowler.providers.alibabacloud.config import ALIBABACLOUD_REGIONS
|
||||
|
||||
|
||||
def init_parser(self):
|
||||
"""
|
||||
Initialize Alibaba Cloud provider CLI argument parser
|
||||
|
||||
This function creates the argument parser for Alibaba Cloud provider and defines
|
||||
all the command-line arguments that can be used when auditing Alibaba Cloud.
|
||||
|
||||
Args:
|
||||
self: The ProwlerArgumentParser instance
|
||||
"""
|
||||
# Create Alibaba Cloud subparser
|
||||
alibabacloud_parser = self.subparsers.add_parser(
|
||||
"alibabacloud",
|
||||
parents=[self.common_providers_parser],
|
||||
help="Alibaba Cloud Provider",
|
||||
)
|
||||
|
||||
# Authentication group
|
||||
alibabacloud_auth = alibabacloud_parser.add_argument_group(
|
||||
"Alibaba Cloud Authentication"
|
||||
)
|
||||
|
||||
alibabacloud_auth.add_argument(
|
||||
"--access-key-id",
|
||||
nargs="?",
|
||||
default=os.environ.get("ALIBABA_CLOUD_ACCESS_KEY_ID"),
|
||||
help="Alibaba Cloud AccessKey ID (also reads from ALIBABA_CLOUD_ACCESS_KEY_ID environment variable)",
|
||||
)
|
||||
|
||||
alibabacloud_auth.add_argument(
|
||||
"--access-key-secret",
|
||||
nargs="?",
|
||||
default=os.environ.get("ALIBABA_CLOUD_ACCESS_KEY_SECRET"),
|
||||
help="Alibaba Cloud AccessKey Secret (also reads from ALIBABA_CLOUD_ACCESS_KEY_SECRET environment variable)",
|
||||
)
|
||||
|
||||
alibabacloud_auth.add_argument(
|
||||
"--security-token",
|
||||
nargs="?",
|
||||
default=os.environ.get("ALIBABA_CLOUD_SECURITY_TOKEN"),
|
||||
help="Alibaba Cloud STS Security Token for temporary credentials (also reads from ALIBABA_CLOUD_SECURITY_TOKEN environment variable)",
|
||||
)
|
||||
|
||||
# RAM Role assumption group
|
||||
alibabacloud_role = alibabacloud_parser.add_argument_group(
|
||||
"Alibaba Cloud RAM Role Assumption"
|
||||
)
|
||||
|
||||
alibabacloud_role.add_argument(
|
||||
"--ram-role-arn",
|
||||
nargs="?",
|
||||
default=None,
|
||||
help="RAM Role ARN to assume for the audit (format: acs:ram::account-id:role/role-name)",
|
||||
)
|
||||
|
||||
alibabacloud_role.add_argument(
|
||||
"--ram-session-name",
|
||||
nargs="?",
|
||||
default="ProwlerAuditSession",
|
||||
help="Session name for RAM role assumption (default: ProwlerAuditSession)",
|
||||
)
|
||||
|
||||
alibabacloud_role.add_argument(
|
||||
"--ram-session-duration",
|
||||
nargs="?",
|
||||
type=int,
|
||||
default=3600,
|
||||
help="Session duration in seconds for RAM role assumption (900-43200, default: 3600)",
|
||||
)
|
||||
|
||||
alibabacloud_role.add_argument(
|
||||
"--ram-external-id",
|
||||
nargs="?",
|
||||
default=None,
|
||||
help="External ID for RAM role assumption (optional)",
|
||||
)
|
||||
|
||||
# Regions group
|
||||
alibabacloud_regions = alibabacloud_parser.add_argument_group(
|
||||
"Alibaba Cloud Regions"
|
||||
)
|
||||
|
||||
alibabacloud_regions.add_argument(
|
||||
"--region-id",
|
||||
"--region-ids",
|
||||
nargs="+",
|
||||
default=[],
|
||||
dest="region_ids",
|
||||
help=f"Alibaba Cloud Region IDs to audit (space-separated). Available regions: {', '.join(ALIBABACLOUD_REGIONS[:10])}... (default: all regions)",
|
||||
)
|
||||
|
||||
alibabacloud_regions.add_argument(
|
||||
"--filter-region",
|
||||
"--filter-regions",
|
||||
nargs="+",
|
||||
default=[],
|
||||
dest="filter_regions",
|
||||
help="Alibaba Cloud Region IDs to exclude from the audit (space-separated)",
|
||||
)
|
||||
|
||||
# Resource filtering group
|
||||
alibabacloud_resources = alibabacloud_parser.add_argument_group(
|
||||
"Alibaba Cloud Resource Filtering"
|
||||
)
|
||||
|
||||
alibabacloud_resources.add_argument(
|
||||
"--resource-tags",
|
||||
nargs="+",
|
||||
default=[],
|
||||
help="Filter resources by tags (format: key=value)",
|
||||
)
|
||||
|
||||
alibabacloud_resources.add_argument(
|
||||
"--resource-ids",
|
||||
nargs="+",
|
||||
default=[],
|
||||
help="Specific resource IDs to audit (space-separated)",
|
||||
)
|
||||
|
||||
|
||||
def validate_arguments(arguments: Namespace) -> tuple[bool, str]:
|
||||
"""
|
||||
Validate Alibaba Cloud provider arguments
|
||||
|
||||
This function validates the command-line arguments provided for the Alibaba Cloud provider.
|
||||
It checks for required credentials, validates region specifications, and ensures
|
||||
configuration coherence.
|
||||
|
||||
Args:
|
||||
arguments: Parsed command-line arguments
|
||||
|
||||
Returns:
|
||||
tuple: (is_valid: bool, error_message: str)
|
||||
- is_valid: True if arguments are valid, False otherwise
|
||||
- error_message: Error description if invalid, empty string if valid
|
||||
"""
|
||||
# Check for required credentials
|
||||
if not arguments.access_key_id or not arguments.access_key_secret:
|
||||
return (
|
||||
False,
|
||||
"Alibaba Cloud AccessKey credentials are required. "
|
||||
"Provide --access-key-id and --access-key-secret, "
|
||||
"or set ALIBABA_CLOUD_ACCESS_KEY_ID and ALIBABA_CLOUD_ACCESS_KEY_SECRET environment variables.",
|
||||
)
|
||||
|
||||
# Validate RAM role session duration
|
||||
if arguments.ram_role_arn:
|
||||
if not (900 <= arguments.ram_session_duration <= 43200):
|
||||
return (
|
||||
False,
|
||||
f"RAM role session duration must be between 900 and 43200 seconds. "
|
||||
f"Provided: {arguments.ram_session_duration}",
|
||||
)
|
||||
|
||||
# Validate region IDs
|
||||
if arguments.region_ids:
|
||||
invalid_regions = [
|
||||
region for region in arguments.region_ids
|
||||
if region not in ALIBABACLOUD_REGIONS
|
||||
]
|
||||
if invalid_regions:
|
||||
return (
|
||||
False,
|
||||
f"Invalid Alibaba Cloud region(s): {', '.join(invalid_regions)}. "
|
||||
f"Valid regions are: {', '.join(ALIBABACLOUD_REGIONS)}",
|
||||
)
|
||||
|
||||
# Validate filter regions
|
||||
if arguments.filter_regions:
|
||||
invalid_filter_regions = [
|
||||
region for region in arguments.filter_regions
|
||||
if region not in ALIBABACLOUD_REGIONS
|
||||
]
|
||||
if invalid_filter_regions:
|
||||
return (
|
||||
False,
|
||||
f"Invalid Alibaba Cloud filter region(s): {', '.join(invalid_filter_regions)}. "
|
||||
f"Valid regions are: {', '.join(ALIBABACLOUD_REGIONS)}",
|
||||
)
|
||||
|
||||
# Validate resource tags format
|
||||
if arguments.resource_tags:
|
||||
for tag in arguments.resource_tags:
|
||||
if "=" not in tag:
|
||||
return (
|
||||
False,
|
||||
f"Invalid resource tag format: '{tag}'. Expected format: key=value",
|
||||
)
|
||||
|
||||
# All validations passed
|
||||
return (True, "")
|
||||
@@ -0,0 +1,13 @@
|
||||
"""Utility classes for Alibaba Cloud checks"""
|
||||
|
||||
from dataclasses import dataclass
|
||||
|
||||
|
||||
@dataclass
|
||||
class GenericAlibabaCloudResource:
|
||||
"""Generic resource for checks that don't have a specific resource type"""
|
||||
|
||||
id: str
|
||||
name: str
|
||||
arn: str
|
||||
region: str
|
||||
@@ -0,0 +1,82 @@
|
||||
"""
|
||||
Alibaba Cloud Provider Mutelist
|
||||
|
||||
This module implements the mutelist functionality for suppressing findings
|
||||
in Alibaba Cloud audits.
|
||||
"""
|
||||
|
||||
from prowler.lib.mutelist.mutelist import Mutelist
|
||||
|
||||
|
||||
class AlibabaCloudMutelist(Mutelist):
|
||||
"""
|
||||
AlibabaCloudMutelist handles finding suppression for Alibaba Cloud resources
|
||||
|
||||
This class extends the base Mutelist class to provide Alibaba Cloud-specific
|
||||
mutelist functionality, allowing users to suppress specific findings based on
|
||||
resource identifiers, regions, checks, or other criteria.
|
||||
|
||||
Example mutelist entry:
|
||||
{
|
||||
"Accounts": ["1234567890"],
|
||||
"Checks": {
|
||||
"ecs_*": {
|
||||
"Regions": ["cn-hangzhou", "cn-shanghai"],
|
||||
"Resources": ["i-abc123", "i-def456"]
|
||||
}
|
||||
}
|
||||
}
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
mutelist_path: str = None,
|
||||
mutelist_content: dict = None,
|
||||
provider: str = "alibabacloud",
|
||||
identity: dict = None,
|
||||
):
|
||||
"""
|
||||
Initialize Alibaba Cloud mutelist
|
||||
|
||||
Args:
|
||||
mutelist_path: Path to the mutelist file (YAML or JSON)
|
||||
mutelist_content: Mutelist content as a dictionary
|
||||
provider: Provider name (default: "alibabacloud")
|
||||
identity: Alibaba Cloud identity information
|
||||
"""
|
||||
super().__init__(
|
||||
mutelist_path=mutelist_path,
|
||||
mutelist_content=mutelist_content,
|
||||
)
|
||||
self.identity = identity
|
||||
self.provider = provider
|
||||
|
||||
def is_finding_muted(
|
||||
self,
|
||||
finding,
|
||||
account_id: str = None,
|
||||
region: str = None,
|
||||
check_id: str = None,
|
||||
resource_id: str = None,
|
||||
) -> bool:
|
||||
"""
|
||||
Check if a finding should be muted based on mutelist rules
|
||||
|
||||
Args:
|
||||
finding: The finding object to check
|
||||
account_id: Alibaba Cloud account ID
|
||||
region: Alibaba Cloud region ID
|
||||
check_id: Check identifier
|
||||
resource_id: Resource identifier
|
||||
|
||||
Returns:
|
||||
bool: True if the finding is muted, False otherwise
|
||||
"""
|
||||
# Use the parent class implementation which handles the core logic
|
||||
return super().is_muted(
|
||||
account_uid=account_id or getattr(finding, "account_uid", None),
|
||||
region=region or getattr(finding, "region", None),
|
||||
check_id=check_id or getattr(finding, "check_metadata", {}).get("CheckID"),
|
||||
resource_id=resource_id or getattr(finding, "resource_uid", None),
|
||||
finding_tags=getattr(finding, "resource_tags", []),
|
||||
)
|
||||
@@ -0,0 +1,146 @@
|
||||
"""
|
||||
Alibaba Cloud Service Base Class
|
||||
|
||||
This module provides the base class for all Alibaba Cloud service implementations.
|
||||
"""
|
||||
|
||||
import threading
|
||||
|
||||
from prowler.lib.logger import logger
|
||||
|
||||
|
||||
class AlibabaCloudService:
|
||||
"""
|
||||
Base class for Alibaba Cloud service implementations
|
||||
|
||||
This class provides common functionality for all Alibaba Cloud services, including:
|
||||
- Regional client management
|
||||
- Multi-threading support for API calls
|
||||
- Error handling patterns
|
||||
- Resource auditing metadata
|
||||
|
||||
Attributes:
|
||||
provider: The Alibaba Cloud provider instance
|
||||
service: Service name (e.g., "ecs", "oss", "ram")
|
||||
account_id: Alibaba Cloud account ID
|
||||
regions: List of regions to audit
|
||||
audit_config: Audit configuration dictionary
|
||||
regional_clients: Dictionary of regional SDK clients
|
||||
"""
|
||||
|
||||
def __init__(self, service: str, provider):
|
||||
"""
|
||||
Initialize the Alibaba Cloud service
|
||||
|
||||
Args:
|
||||
service: Service identifier (e.g., "ecs", "oss")
|
||||
provider: AlibabaCloudProvider instance
|
||||
"""
|
||||
self.provider = provider
|
||||
self.service = service
|
||||
self.account_id = provider.identity.account_id
|
||||
self.regions = provider._regions
|
||||
self.audit_config = provider.audit_config
|
||||
self.regional_clients = {}
|
||||
|
||||
logger.info(f"Initializing Alibaba Cloud {service.upper()} service")
|
||||
|
||||
def __threading_call__(self, call, iterator):
|
||||
"""
|
||||
Execute function calls in parallel using threading
|
||||
|
||||
This method is used to parallelize API calls across regions or resources
|
||||
to improve audit performance.
|
||||
|
||||
Args:
|
||||
call: Function to execute
|
||||
iterator: Iterable of arguments to pass to the function
|
||||
|
||||
Example:
|
||||
self.__threading_call__(self._describe_instances, self.regions)
|
||||
"""
|
||||
threads = []
|
||||
for item in iterator:
|
||||
thread = threading.Thread(target=call, args=(item,))
|
||||
threads.append(thread)
|
||||
thread.start()
|
||||
|
||||
# Wait for all threads to complete
|
||||
for thread in threads:
|
||||
thread.join()
|
||||
|
||||
def _create_regional_client(self, region: str, _endpoint_override: str = None):
|
||||
"""
|
||||
Create a regional Alibaba Cloud SDK client
|
||||
|
||||
This method should be overridden by service implementations to create
|
||||
service-specific clients.
|
||||
|
||||
Args:
|
||||
region: Region identifier
|
||||
_endpoint_override: Optional endpoint override URL (unused in base)
|
||||
|
||||
Returns:
|
||||
SDK client instance for the specified region
|
||||
"""
|
||||
raise NotImplementedError(
|
||||
f"Service {self.service} must implement _create_regional_client()"
|
||||
)
|
||||
|
||||
def _list_resources(self):
|
||||
"""
|
||||
List all resources for this service
|
||||
|
||||
This method should be overridden by service implementations to list
|
||||
service-specific resources.
|
||||
"""
|
||||
raise NotImplementedError(
|
||||
f"Service {self.service} must implement _list_resources()"
|
||||
)
|
||||
|
||||
def _get_resource_details(self, resource):
|
||||
"""
|
||||
Get detailed information about a resource
|
||||
|
||||
This method can be overridden by service implementations to fetch
|
||||
additional resource details.
|
||||
|
||||
Args:
|
||||
resource: Resource identifier or object
|
||||
"""
|
||||
|
||||
def _handle_api_error(self, error: Exception, operation: str, region: str = None):
|
||||
"""
|
||||
Handle Alibaba Cloud API errors with consistent logging
|
||||
|
||||
Args:
|
||||
error: The exception that occurred
|
||||
operation: The API operation that failed
|
||||
region: The region where the error occurred (if applicable)
|
||||
"""
|
||||
region_info = f" in region {region}" if region else ""
|
||||
logger.warning(
|
||||
f"{self.service.upper()} {operation} failed{region_info}: {str(error)}"
|
||||
)
|
||||
|
||||
def generate_resource_arn(
|
||||
self, resource_type: str, resource_id: str, region: str = ""
|
||||
) -> str:
|
||||
"""
|
||||
Generate Alibaba Cloud Resource Name (ARN) in ACS format
|
||||
|
||||
Format: acs:{service}:{region}:{account-id}:{resource-type}/{resource-id}
|
||||
|
||||
Args:
|
||||
resource_type: Type of resource (e.g., "instance", "bucket")
|
||||
resource_id: Resource identifier
|
||||
region: Region identifier (optional for global resources)
|
||||
|
||||
Returns:
|
||||
str: Formatted ARN string
|
||||
|
||||
Example:
|
||||
arn = self.generate_resource_arn("instance", "i-abc123", "cn-hangzhou")
|
||||
# Returns: "acs:ecs:cn-hangzhou:123456789:instance/i-abc123"
|
||||
"""
|
||||
return f"acs:{self.service}:{region}:{self.account_id}:{resource_type}/{resource_id}"
|
||||
@@ -0,0 +1,129 @@
|
||||
"""
|
||||
Alibaba Cloud Provider Models
|
||||
|
||||
This module contains data models for the Alibaba Cloud provider.
|
||||
"""
|
||||
|
||||
from argparse import Namespace
|
||||
from dataclasses import dataclass
|
||||
from typing import Optional
|
||||
|
||||
from prowler.providers.common.models import ProviderOutputOptions
|
||||
|
||||
|
||||
@dataclass
|
||||
class AlibabaCloudIdentityInfo:
|
||||
"""
|
||||
AlibabaCloudIdentityInfo contains the Alibaba Cloud identity information
|
||||
|
||||
Attributes:
|
||||
account_id: Alibaba Cloud account ID
|
||||
account_arn: Alibaba Cloud account ARN
|
||||
user_id: RAM user ID (optional)
|
||||
user_name: RAM user name (optional)
|
||||
account_name: Account alias/name (optional)
|
||||
"""
|
||||
account_id: str
|
||||
account_arn: str
|
||||
user_id: Optional[str] = None
|
||||
user_name: Optional[str] = None
|
||||
account_name: Optional[str] = None
|
||||
|
||||
|
||||
@dataclass
|
||||
class AlibabaCloudRegion:
|
||||
"""
|
||||
AlibabaCloudRegion contains region information
|
||||
|
||||
Attributes:
|
||||
region_id: Region identifier (e.g., "cn-hangzhou")
|
||||
local_name: Localized region name
|
||||
region_endpoint: Region endpoint URL
|
||||
"""
|
||||
region_id: str
|
||||
local_name: str
|
||||
region_endpoint: Optional[str] = None
|
||||
|
||||
|
||||
@dataclass
|
||||
class AlibabaCloudCredentials:
|
||||
"""
|
||||
AlibabaCloudCredentials contains authentication credentials
|
||||
|
||||
Attributes:
|
||||
access_key_id: AccessKey ID
|
||||
access_key_secret: AccessKey Secret
|
||||
security_token: STS security token (optional)
|
||||
expiration: Token expiration timestamp (optional)
|
||||
"""
|
||||
access_key_id: str
|
||||
access_key_secret: str
|
||||
security_token: Optional[str] = None
|
||||
expiration: Optional[str] = None
|
||||
|
||||
|
||||
@dataclass
|
||||
class AlibabaCloudAssumeRoleInfo:
|
||||
"""
|
||||
AlibabaCloudAssumeRoleInfo contains RAM role assumption information
|
||||
|
||||
Attributes:
|
||||
role_arn: RAM role ARN to assume
|
||||
role_session_name: Session name for the assumed role
|
||||
external_id: External ID for role assumption (optional)
|
||||
session_duration: Duration in seconds (900-43200, default 3600)
|
||||
"""
|
||||
role_arn: str
|
||||
role_session_name: str
|
||||
external_id: Optional[str] = None
|
||||
session_duration: int = 3600
|
||||
|
||||
|
||||
@dataclass
|
||||
class AlibabaCloudSession:
|
||||
"""
|
||||
AlibabaCloudSession contains the session configuration
|
||||
|
||||
Attributes:
|
||||
credentials: Alibaba Cloud credentials
|
||||
region_id: Default region for the session
|
||||
"""
|
||||
credentials: AlibabaCloudCredentials
|
||||
region_id: str = "cn-hangzhou"
|
||||
|
||||
|
||||
class AlibabaCloudOutputOptions(ProviderOutputOptions):
|
||||
"""
|
||||
AlibabaCloudOutputOptions contains the output configuration for Alibaba Cloud
|
||||
|
||||
This class extends ProviderOutputOptions to provide Alibaba Cloud-specific
|
||||
output filename generation based on the account ID.
|
||||
"""
|
||||
|
||||
def __init__(self, arguments: Namespace, bulk_checks_metadata: dict, identity: AlibabaCloudIdentityInfo):
|
||||
"""
|
||||
Initialize Alibaba Cloud output options
|
||||
|
||||
Args:
|
||||
arguments: Command-line arguments
|
||||
bulk_checks_metadata: Metadata for all checks
|
||||
identity: Alibaba Cloud identity information
|
||||
"""
|
||||
# Call parent class init
|
||||
super().__init__(arguments, bulk_checks_metadata)
|
||||
|
||||
# Import here to avoid circular dependency
|
||||
from prowler.config.config import output_file_timestamp
|
||||
|
||||
# Check if custom output filename was provided
|
||||
if (
|
||||
not hasattr(arguments, "output_filename")
|
||||
or arguments.output_filename is None
|
||||
):
|
||||
# Use account ID for output filename
|
||||
account_identifier = identity.account_id
|
||||
self.output_filename = (
|
||||
f"prowler-output-{account_identifier}-{output_file_timestamp}"
|
||||
)
|
||||
else:
|
||||
self.output_filename = arguments.output_filename
|
||||
@@ -0,0 +1 @@
|
||||
"""Alibaba Cloud ACK (Container Service for Kubernetes) module"""
|
||||
@@ -0,0 +1,6 @@
|
||||
"""Alibaba Cloud ACK Client Singleton"""
|
||||
|
||||
from prowler.providers.alibabacloud.services.ack.ack_service import ACK
|
||||
from prowler.providers.common.provider import Provider
|
||||
|
||||
ack_client = ACK(Provider.get_global_provider())
|
||||
@@ -0,0 +1 @@
|
||||
"""ACK Audit Logging Check"""
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"Provider": "alibabacloud",
|
||||
"CheckID": "ack_cluster_audit_log",
|
||||
"CheckTitle": "Check ACK Audit Logging",
|
||||
"CheckType": [],
|
||||
"ServiceName": "ack",
|
||||
"SubServiceName": "",
|
||||
"ResourceIdTemplate": "arn",
|
||||
"Severity": "medium",
|
||||
"ResourceType": "Cluster",
|
||||
"Description": "Checks ACK Audit Logging.",
|
||||
"Risk": "",
|
||||
"RelatedUrl": "",
|
||||
"Remediation": {"Code": {"CLI": "", "NativeIaC": "", "Other": "", "Terraform": ""}, "Recommendation": {"Text": "", "Url": ""}},
|
||||
"Categories": [],
|
||||
"DependsOn": [],
|
||||
"RelatedTo": [],
|
||||
"Notes": ""
|
||||
}
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
from prowler.lib.check.models import Check, Check_Report_AlibabaCloud
|
||||
from prowler.providers.alibabacloud.services.ack.ack_client import ack_client
|
||||
|
||||
|
||||
class ack_cluster_audit_log(Check):
|
||||
def execute(self):
|
||||
findings = []
|
||||
for cluster in ack_client.clusters.values():
|
||||
report = Check_Report_AlibabaCloud(
|
||||
metadata=self.metadata(), resource=cluster
|
||||
)
|
||||
report.status = "FAIL"
|
||||
report.status_extended = f"ACK cluster {cluster.cluster_name} does not have audit logging enabled."
|
||||
if cluster.audit_log_enabled:
|
||||
report.status = "PASS"
|
||||
report.status_extended = (
|
||||
f"ACK cluster {cluster.cluster_name} has audit logging enabled."
|
||||
)
|
||||
findings.append(report)
|
||||
return findings
|
||||
@@ -0,0 +1 @@
|
||||
"""ACK Encryption Check"""
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"Provider": "alibabacloud",
|
||||
"CheckID": "ack_cluster_encryption",
|
||||
"CheckTitle": "Check ACK Encryption",
|
||||
"CheckType": [],
|
||||
"ServiceName": "ack",
|
||||
"SubServiceName": "",
|
||||
"ResourceIdTemplate": "arn",
|
||||
"Severity": "high",
|
||||
"ResourceType": "Cluster",
|
||||
"Description": "Checks ACK Encryption.",
|
||||
"Risk": "",
|
||||
"RelatedUrl": "",
|
||||
"Remediation": {"Code": {"CLI": "", "NativeIaC": "", "Other": "", "Terraform": ""}, "Recommendation": {"Text": "", "Url": ""}},
|
||||
"Categories": [],
|
||||
"DependsOn": [],
|
||||
"RelatedTo": [],
|
||||
"Notes": ""
|
||||
}
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
from prowler.lib.check.models import Check, Check_Report_AlibabaCloud
|
||||
from prowler.providers.alibabacloud.services.ack.ack_client import ack_client
|
||||
|
||||
|
||||
class ack_cluster_encryption(Check):
|
||||
def execute(self):
|
||||
findings = []
|
||||
for cluster in ack_client.clusters.values():
|
||||
report = Check_Report_AlibabaCloud(
|
||||
metadata=self.metadata(), resource=cluster
|
||||
)
|
||||
report.status = "FAIL"
|
||||
report.status_extended = (
|
||||
f"ACK cluster {cluster.cluster_name} does not have encryption enabled."
|
||||
)
|
||||
if cluster.encryption_enabled:
|
||||
report.status = "PASS"
|
||||
report.status_extended = (
|
||||
f"ACK cluster {cluster.cluster_name} has encryption enabled."
|
||||
)
|
||||
findings.append(report)
|
||||
return findings
|
||||
@@ -0,0 +1 @@
|
||||
"""ACK Network Policy Check"""
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"Provider": "alibabacloud",
|
||||
"CheckID": "ack_cluster_network_policy",
|
||||
"CheckTitle": "Check ACK Network Policy",
|
||||
"CheckType": [],
|
||||
"ServiceName": "ack",
|
||||
"SubServiceName": "",
|
||||
"ResourceIdTemplate": "arn",
|
||||
"Severity": "medium",
|
||||
"ResourceType": "Cluster",
|
||||
"Description": "Checks ACK Network Policy.",
|
||||
"Risk": "",
|
||||
"RelatedUrl": "",
|
||||
"Remediation": {"Code": {"CLI": "", "NativeIaC": "", "Other": "", "Terraform": ""}, "Recommendation": {"Text": "", "Url": ""}},
|
||||
"Categories": [],
|
||||
"DependsOn": [],
|
||||
"RelatedTo": [],
|
||||
"Notes": ""
|
||||
}
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
from prowler.lib.check.models import Check, Check_Report_AlibabaCloud
|
||||
from prowler.providers.alibabacloud.services.ack.ack_client import ack_client
|
||||
|
||||
|
||||
class ack_cluster_network_policy(Check):
|
||||
def execute(self):
|
||||
findings = []
|
||||
for cluster in ack_client.clusters.values():
|
||||
report = Check_Report_AlibabaCloud(
|
||||
metadata=self.metadata(), resource=cluster
|
||||
)
|
||||
report.status = "FAIL"
|
||||
report.status_extended = f"ACK cluster {cluster.cluster_name} does not have network policy support enabled."
|
||||
if cluster.network_policy_enabled:
|
||||
report.status = "PASS"
|
||||
report.status_extended = f"ACK cluster {cluster.cluster_name} has network policy support enabled."
|
||||
findings.append(report)
|
||||
return findings
|
||||
@@ -0,0 +1 @@
|
||||
"""ACK Private Zone Check"""
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"Provider": "alibabacloud",
|
||||
"CheckID": "ack_cluster_private_zone",
|
||||
"CheckTitle": "Check ACK Private Zone",
|
||||
"CheckType": [],
|
||||
"ServiceName": "ack",
|
||||
"SubServiceName": "",
|
||||
"ResourceIdTemplate": "arn",
|
||||
"Severity": "low",
|
||||
"ResourceType": "Cluster",
|
||||
"Description": "Checks ACK Private Zone.",
|
||||
"Risk": "",
|
||||
"RelatedUrl": "",
|
||||
"Remediation": {"Code": {"CLI": "", "NativeIaC": "", "Other": "", "Terraform": ""}, "Recommendation": {"Text": "", "Url": ""}},
|
||||
"Categories": [],
|
||||
"DependsOn": [],
|
||||
"RelatedTo": [],
|
||||
"Notes": ""
|
||||
}
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
from prowler.lib.check.models import Check, Check_Report_AlibabaCloud
|
||||
from prowler.providers.alibabacloud.services.ack.ack_client import ack_client
|
||||
|
||||
|
||||
class ack_cluster_private_zone(Check):
|
||||
def execute(self):
|
||||
findings = []
|
||||
for cluster in ack_client.clusters.values():
|
||||
report = Check_Report_AlibabaCloud(
|
||||
metadata=self.metadata(), resource=cluster
|
||||
)
|
||||
report.status = "FAIL"
|
||||
report.status_extended = (
|
||||
f"ACK cluster {cluster.cluster_name} does not have PrivateZone enabled."
|
||||
)
|
||||
if cluster.private_zone_enabled:
|
||||
report.status = "PASS"
|
||||
report.status_extended = (
|
||||
f"ACK cluster {cluster.cluster_name} has PrivateZone enabled."
|
||||
)
|
||||
findings.append(report)
|
||||
return findings
|
||||
@@ -0,0 +1 @@
|
||||
"""ACK Public Access Check"""
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"Provider": "alibabacloud",
|
||||
"CheckID": "ack_cluster_public_access",
|
||||
"CheckTitle": "Check ACK Public Access",
|
||||
"CheckType": [],
|
||||
"ServiceName": "ack",
|
||||
"SubServiceName": "",
|
||||
"ResourceIdTemplate": "arn",
|
||||
"Severity": "high",
|
||||
"ResourceType": "Cluster",
|
||||
"Description": "Checks ACK Public Access.",
|
||||
"Risk": "",
|
||||
"RelatedUrl": "",
|
||||
"Remediation": {"Code": {"CLI": "", "NativeIaC": "", "Other": "", "Terraform": ""}, "Recommendation": {"Text": "", "Url": ""}},
|
||||
"Categories": [],
|
||||
"DependsOn": [],
|
||||
"RelatedTo": [],
|
||||
"Notes": ""
|
||||
}
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
from prowler.lib.check.models import Check, Check_Report_AlibabaCloud
|
||||
from prowler.providers.alibabacloud.services.ack.ack_client import ack_client
|
||||
|
||||
|
||||
class ack_cluster_public_access(Check):
|
||||
def execute(self):
|
||||
findings = []
|
||||
for cluster in ack_client.clusters.values():
|
||||
report = Check_Report_AlibabaCloud(
|
||||
metadata=self.metadata(), resource=cluster
|
||||
)
|
||||
report.status = "FAIL"
|
||||
report.status_extended = (
|
||||
f"ACK cluster {cluster.cluster_name} API server is publicly accessible."
|
||||
)
|
||||
if not cluster.public_access:
|
||||
report.status = "PASS"
|
||||
report.status_extended = f"ACK cluster {cluster.cluster_name} API server is not publicly accessible."
|
||||
findings.append(report)
|
||||
return findings
|
||||
@@ -0,0 +1 @@
|
||||
"""ACK RBAC Check"""
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"Provider": "alibabacloud",
|
||||
"CheckID": "ack_cluster_rbac",
|
||||
"CheckTitle": "Check ACK RBAC",
|
||||
"CheckType": [],
|
||||
"ServiceName": "ack",
|
||||
"SubServiceName": "",
|
||||
"ResourceIdTemplate": "arn",
|
||||
"Severity": "medium",
|
||||
"ResourceType": "Cluster",
|
||||
"Description": "Checks ACK RBAC.",
|
||||
"Risk": "",
|
||||
"RelatedUrl": "",
|
||||
"Remediation": {"Code": {"CLI": "", "NativeIaC": "", "Other": "", "Terraform": ""}, "Recommendation": {"Text": "", "Url": ""}},
|
||||
"Categories": [],
|
||||
"DependsOn": [],
|
||||
"RelatedTo": [],
|
||||
"Notes": ""
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
from prowler.lib.check.models import Check, Check_Report_AlibabaCloud
|
||||
from prowler.providers.alibabacloud.services.ack.ack_client import ack_client
|
||||
|
||||
|
||||
class ack_cluster_rbac(Check):
|
||||
def execute(self):
|
||||
findings = []
|
||||
for cluster in ack_client.clusters.values():
|
||||
report = Check_Report_AlibabaCloud(
|
||||
metadata=self.metadata(), resource=cluster
|
||||
)
|
||||
report.status = "FAIL"
|
||||
report.status_extended = (
|
||||
f"ACK cluster {cluster.cluster_name} does not have RBAC enabled."
|
||||
)
|
||||
if cluster.rbac_enabled:
|
||||
report.status = "PASS"
|
||||
report.status_extended = (
|
||||
f"ACK cluster {cluster.cluster_name} has RBAC enabled."
|
||||
)
|
||||
findings.append(report)
|
||||
return findings
|
||||
@@ -0,0 +1 @@
|
||||
"""ACK Security Group Check"""
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"Provider": "alibabacloud",
|
||||
"CheckID": "ack_cluster_security_group",
|
||||
"CheckTitle": "Check ACK Security Group",
|
||||
"CheckType": [],
|
||||
"ServiceName": "ack",
|
||||
"SubServiceName": "",
|
||||
"ResourceIdTemplate": "arn",
|
||||
"Severity": "medium",
|
||||
"ResourceType": "Cluster",
|
||||
"Description": "Checks ACK Security Group.",
|
||||
"Risk": "",
|
||||
"RelatedUrl": "",
|
||||
"Remediation": {"Code": {"CLI": "", "NativeIaC": "", "Other": "", "Terraform": ""}, "Recommendation": {"Text": "", "Url": ""}},
|
||||
"Categories": [],
|
||||
"DependsOn": [],
|
||||
"RelatedTo": [],
|
||||
"Notes": ""
|
||||
}
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
from prowler.lib.check.models import Check, Check_Report_AlibabaCloud
|
||||
from prowler.providers.alibabacloud.services.ack.ack_client import ack_client
|
||||
|
||||
|
||||
class ack_cluster_security_group(Check):
|
||||
def execute(self):
|
||||
findings = []
|
||||
for cluster in ack_client.clusters.values():
|
||||
report = Check_Report_AlibabaCloud(
|
||||
metadata=self.metadata(), resource=cluster
|
||||
)
|
||||
report.status = "FAIL"
|
||||
report.status_extended = f"ACK cluster {cluster.cluster_name} does not have a security group configured."
|
||||
if cluster.security_group_id:
|
||||
report.status = "PASS"
|
||||
report.status_extended = f"ACK cluster {cluster.cluster_name} has security group {cluster.security_group_id} configured."
|
||||
findings.append(report)
|
||||
return findings
|
||||
@@ -0,0 +1 @@
|
||||
"""ACK VPC Check"""
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"Provider": "alibabacloud",
|
||||
"CheckID": "ack_cluster_vpc",
|
||||
"CheckTitle": "Check ACK VPC",
|
||||
"CheckType": [],
|
||||
"ServiceName": "ack",
|
||||
"SubServiceName": "",
|
||||
"ResourceIdTemplate": "arn",
|
||||
"Severity": "medium",
|
||||
"ResourceType": "Cluster",
|
||||
"Description": "Checks ACK VPC.",
|
||||
"Risk": "",
|
||||
"RelatedUrl": "",
|
||||
"Remediation": {"Code": {"CLI": "", "NativeIaC": "", "Other": "", "Terraform": ""}, "Recommendation": {"Text": "", "Url": ""}},
|
||||
"Categories": [],
|
||||
"DependsOn": [],
|
||||
"RelatedTo": [],
|
||||
"Notes": ""
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
from prowler.lib.check.models import Check, Check_Report_AlibabaCloud
|
||||
from prowler.providers.alibabacloud.services.ack.ack_client import ack_client
|
||||
|
||||
|
||||
class ack_cluster_vpc(Check):
|
||||
def execute(self):
|
||||
findings = []
|
||||
for cluster in ack_client.clusters.values():
|
||||
report = Check_Report_AlibabaCloud(
|
||||
metadata=self.metadata(), resource=cluster
|
||||
)
|
||||
report.status = "FAIL"
|
||||
report.status_extended = (
|
||||
f"ACK cluster {cluster.cluster_name} is not deployed in a VPC."
|
||||
)
|
||||
if cluster.vpc_id:
|
||||
report.status = "PASS"
|
||||
report.status_extended = f"ACK cluster {cluster.cluster_name} is deployed in VPC {cluster.vpc_id}."
|
||||
findings.append(report)
|
||||
return findings
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user