Compare commits

..

51 Commits

Author SHA1 Message Date
Chandrapal Badshah 0c669a069b chore: add bedrock test cases 2025-10-26 21:59:50 +05:30
Chandrapal Badshah 4cb2778bfa feat: add amazon bedrock 2025-10-26 21:59:50 +05:30
Chandrapal Badshah dfe2b43ea5 fix: allow comma-separated values in fields[lighthouse-providers] filter 2025-10-26 21:46:07 +05:30
Chandrapal Badshah c42351335d chore: resolve conflicts 2025-10-24 14:46:46 +05:30
Chandrapal Badshah f5d73c2224 refactor: lighthouse migration script to migrate all lighthouse configs 2025-10-21 20:56:26 +05:30
Chandrapal Badshah e981e90f89 test: add test for tenant defaults update on provider delete 2025-10-20 20:17:50 +05:30
Chandrapal Badshah 59255c0b76 fix: move lighthouse provider delete cleanup to pre_delete signal 2025-10-20 20:14:20 +05:30
Chandrapal Badshah c64c86d8a3 chore: use API logger in migration file 2025-10-20 19:01:15 +05:30
Chandrapal Badshah 28ce5c9b39 chore: deprecate old lighthouse endpoints 2025-10-20 18:51:02 +05:30
Chandrapal Badshah 9fe53cf13d chore: rename lighthouse config endpoint 2025-10-20 14:15:28 +05:30
Adrián Jesús Peña Rodríguez 77fa5d1644 Merge branch 'master' into PRWLR-7835-multi-provider-support 2025-10-17 12:04:10 +02:00
Chandrapal Badshah a77297ff4c feat: add model_name property to models 2025-10-15 15:25:51 +05:30
Chandrapal Badshah 8e5bd7db18 chore: rename lighthouse migration file 2025-10-15 13:09:34 +05:30
Chandrapal Badshah f1f6120295 fix: update lighthouse migration to remove manual sql queries 2025-10-14 17:02:28 +05:30
Chandrapal Badshah 201c5211fe fix: rename migration file name 2025-10-14 13:50:03 +05:30
Chandrapal Badshah 6ebc67b776 chore: ruff format 2025-10-14 13:34:29 +05:30
Chandrapal Badshah 1a0fac0db8 feat: copy data to new lighthouse config tables during migration 2025-10-14 13:29:55 +05:30
Chandrapal Badshah 8ac481f610 chore: update specs v1 file 2025-10-14 13:29:55 +05:30
Chandrapal Badshah cfa4260c4b chore: update lighthouse API docs 2025-10-14 13:29:55 +05:30
Chandrapal Badshah bc60a35318 fix: update lighthouse migration file 2025-10-14 13:29:55 +05:30
Chandrapal Badshah 4c078e8d66 chore: ruff format 2025-10-14 13:29:55 +05:30
Chandrapal Badshah 241bd3e437 chore: recreate lighthouse migrations file 2025-10-14 13:29:55 +05:30
Chandrapal Badshah 7669f00095 chore: update lighthouse tenant test cases 2025-10-14 13:29:55 +05:30
Chandrapal Badshah 218f8511f5 chore: refactor lighthouse config singleton code 2025-10-14 13:29:55 +05:30
Chandrapal Badshah d70e8f0633 chore: refactor to remove unnecessary code 2025-10-14 13:29:55 +05:30
Chandrapal Badshah e30101b789 feat: add lighthouse serializer utils 2025-10-14 13:29:55 +05:30
Chandrapal Badshah dd7fed06ac fix: rename class to differentiate lighthouse providers 2025-10-14 13:29:55 +05:30
Chandrapal Badshah 29ab107edf fix: update lighthouse test cases 2025-10-14 13:29:55 +05:30
Chandrapal Badshah a56686bf1a fix: move lighthouse validation from model to serializer 2025-10-14 13:29:55 +05:30
Chandrapal Badshah fb870711d9 chore: update docstring in lighthouse provider job 2025-10-14 13:29:55 +05:30
Chandrapal Badshah 774211df2b feat(lighthouse): auto-create tenant config on PATCH 2025-10-14 13:29:55 +05:30
Chandrapal Badshah 0f06ec2593 fix: update migration file 2025-10-14 13:29:55 +05:30
Chandrapal Badshah 3e92af063d fix: update test case to mock llm provider config and models 2025-10-14 13:29:55 +05:30
Chandrapal Badshah cc4d6726b4 chore: format using ruff 2025-10-14 13:29:54 +05:30
Chandrapal Badshah b5ac1eca73 chore: update api changelog 2025-10-14 13:29:52 +05:30
Chandrapal Badshah 6bec69fa81 fix: update validation for default_provider and default_models in tenant config 2025-10-14 13:29:24 +05:30
Chandrapal Badshah 2c71a15a12 feat: add tasks to validate llm provider creds and refresh models 2025-10-14 13:29:24 +05:30
Chandrapal Badshah ed12d2f418 feat: add filter to models api 2025-10-14 13:29:24 +05:30
Chandrapal Badshah 5602a4182d feat: add option to filter providers 2025-10-14 13:29:24 +05:30
Chandrapal Badshah f8fde7018c chore: refactor LighthouseProviderConfig model code 2025-10-14 13:29:24 +05:30
Chandrapal Badshah 41b95da6e6 chore: add few more lighthouse provider config tests 2025-10-14 13:29:24 +05:30
Chandrapal Badshah 68e2343ca2 chore: refactor code to deduplicate validation of llm provider creds 2025-10-14 13:29:24 +05:30
Chandrapal Badshah f86b73190b feat: add lighthouse provider config tests 2025-10-14 13:29:24 +05:30
Chandrapal Badshah 710df66fc4 fix: add validation for default_provider in tenant config 2025-10-14 13:29:24 +05:30
Chandrapal Badshah a7c06a88e8 chore: add tests for lighthouse tenant config 2025-10-14 13:29:24 +05:30
Chandrapal Badshah 8252fddd7a fix: implement singleton pattern for lighthouse tenant config 2025-10-14 13:29:24 +05:30
Chandrapal Badshah 2d25dbf2b5 fix: update lighthouse tenant config get_url logic 2025-10-14 13:29:24 +05:30
Chandrapal Badshah 042d8aefea fix: add permissions to new lighthouse tables 2025-10-14 13:29:24 +05:30
Chandrapal Badshah 5c691028b9 chore: remove duplicate lighthouse settings model 2025-10-14 13:29:24 +05:30
Chandrapal Badshah cacef6f0cd feat: add lighthouse migration file 2025-10-14 13:29:24 +05:30
Chandrapal Badshah a02cc6364b chore: resolve conflicts 2025-10-14 13:29:19 +05:30
75 changed files with 810 additions and 5392 deletions
+3 -17
View File
@@ -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/"
+2 -16
View File
@@ -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/"
-34
View File
@@ -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
-80
View File
@@ -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'
+15 -13
View File
@@ -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
+43 -38
View File
@@ -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}}"
+24 -23
View File
@@ -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}}"
-1
View File
@@ -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
-6
View File
@@ -39,12 +39,6 @@ secrets-*/
# JUnit Reports
junit-reports/
# Test and coverage artifacts
*_coverage.xml
pytest_*.xml
.coverage
htmlcov/
# VSCode files
.vscode/
+1 -5
View File
@@ -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
@@ -19,6 +14,7 @@ All notable changes to the **Prowler API** are documented in this file.
- Support for `passed_findings` and `total_findings` fields in compliance requirement overview for accurate Prowler ThreatScore calculation [(#8582)](https://github.com/prowler-cloud/prowler/pull/8582)
- PDF reporting for Prowler ThreatScore [(#8867)](https://github.com/prowler-cloud/prowler/pull/8867)
- Database read replica support [(#8869)](https://github.com/prowler-cloud/prowler/pull/8869)
- Support for configuring multiple LLM providers [(#8772)](https://github.com/prowler-cloud/prowler/pull/8772)
- Support Common Cloud Controls for AWS, Azure and GCP [(#8000)](https://github.com/prowler-cloud/prowler/pull/8000)
- Add `provider_id__in` filter support to findings and findings severity overview endpoints [(#8951)](https://github.com/prowler-cloud/prowler/pull/8951)
+1 -1
View File
@@ -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"
@@ -0,0 +1,21 @@
# Generated by Django 5.1.12 on 2025-10-14 11:46
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("api", "0050_lighthouse_multi_llm"),
]
operations = [
migrations.AlterField(
model_name="lighthouseproviderconfiguration",
name="provider_type",
field=models.CharField(
choices=[("openai", "OpenAI"), ("bedrock", "AWS Bedrock")],
help_text="LLM provider name",
max_length=50,
),
)
]
+1
View File
@@ -1970,6 +1970,7 @@ class LighthouseProviderConfiguration(RowLevelSecurityProtectedModel):
class LLMProviderChoices(models.TextChoices):
OPENAI = "openai", _("OpenAI")
BEDROCK = "bedrock", _("AWS Bedrock")
id = models.UUIDField(primary_key=True, default=uuid4, editable=False)
inserted_at = models.DateTimeField(auto_now_add=True, editable=False)
+1 -1
View File
@@ -1,7 +1,7 @@
openapi: 3.0.3
info:
title: Prowler API
version: 1.15.0
version: 1.14.0
description: |-
Prowler API specification.
+195
View File
@@ -9186,3 +9186,198 @@ class TestLighthouseProviderConfigViewSet:
# Unrelated entries should remain untouched
assert cfg.default_models.get("other") == "model-x"
@pytest.mark.parametrize(
"credentials",
[
{}, # empty credentials
{
"access_key_id": "AKIAIOSFODNN7EXAMPLE"
}, # missing secret_access_key and region
{
"secret_access_key": "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"
}, # missing access_key_id and region
{
"access_key_id": "AKIAIOSFODNN7EXAMPLE",
"secret_access_key": "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY",
}, # missing region
{ # invalid access_key_id format (not starting with AKIA)
"access_key_id": "ABCD0123456789ABCDEF",
"secret_access_key": "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY",
"region": "us-east-1",
},
{ # invalid access_key_id format (wrong length)
"access_key_id": "AKIAIOSFODNN7EXAMPL",
"secret_access_key": "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY",
"region": "us-east-1",
},
{ # invalid secret_access_key format (wrong length)
"access_key_id": "AKIAIOSFODNN7EXAMPLE",
"secret_access_key": "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEK",
"region": "us-east-1",
},
{ # invalid region format
"access_key_id": "AKIAIOSFODNN7EXAMPLE",
"secret_access_key": "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY",
"region": "invalid-region",
},
{ # invalid region format (uppercase)
"access_key_id": "AKIAIOSFODNN7EXAMPLE",
"secret_access_key": "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY",
"region": "US-EAST-1",
},
],
)
def test_bedrock_invalid_credentials(self, authenticated_client, credentials):
"""Bedrock provider with invalid credentials should error"""
payload = {
"data": {
"type": "lighthouse-providers",
"attributes": {
"provider_type": "bedrock",
"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_bedrock_valid_credentials_success(self, authenticated_client):
"""Bedrock provider with valid AWS credentials should succeed and mask credentials"""
valid_credentials = {
"access_key_id": "AKIAIOSFODNN7EXAMPLE",
"secret_access_key": "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY",
"region": "us-east-1",
}
payload = {
"data": {
"type": "lighthouse-providers",
"attributes": {
"provider_type": "bedrock",
"credentials": valid_credentials,
},
}
}
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"]
# Verify credentials are returned masked
masked_creds = data["attributes"].get("credentials")
assert masked_creds is not None
assert "access_key_id" in masked_creds
assert "secret_access_key" in masked_creds
assert "region" in masked_creds
# Verify all characters are masked with asterisks
assert all(c == "*" for c in masked_creds["access_key_id"])
assert all(c == "*" for c in masked_creds["secret_access_key"])
def test_bedrock_provider_duplicate_per_tenant(self, authenticated_client):
"""Creating a second Bedrock provider for same tenant should fail"""
valid_credentials = {
"access_key_id": "AKIAIOSFODNN7EXAMPLE",
"secret_access_key": "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY",
"region": "us-west-2",
}
payload = {
"data": {
"type": "lighthouse-providers",
"attributes": {
"provider_type": "bedrock",
"credentials": valid_credentials,
},
}
}
# 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_bedrock_patch_credentials_and_fields_filter(self, authenticated_client):
"""PATCH credentials and verify fields filter returns decrypted values"""
valid_credentials = {
"access_key_id": "AKIAIOSFODNN7EXAMPLE",
"secret_access_key": "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY",
"region": "eu-west-1",
}
create_payload = {
"data": {
"type": "lighthouse-providers",
"attributes": {
"provider_type": "bedrock",
"credentials": valid_credentials,
},
}
}
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"]
# Update credentials with new valid ones
new_credentials = {
"access_key_id": "AKIAZZZZZZZZZZZZZZZZ",
"secret_access_key": "aBcDeFgHiJkLmNoPqRsTuVwXyZ0123456789+/==",
"region": "ap-south-1",
}
patch_payload = {
"data": {
"type": "lighthouse-providers",
"id": provider_id,
"attributes": {
"credentials": new_credentials,
"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["is_active"] is False
# 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"]
assert all(c == "*" for c in masked["access_key_id"])
assert all(c == "*" for c in masked["secret_access_key"])
# Fields filter should return decrypted credentials
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["access_key_id"] == new_credentials["access_key_id"]
assert creds["secret_access_key"] == new_credentials["secret_access_key"]
assert creds["region"] == new_credentials["region"]
@@ -11,3 +11,42 @@ class OpenAICredentialsSerializer(serializers.Serializer):
if not re.match(pattern, value or ""):
raise serializers.ValidationError("Invalid OpenAI API key format.")
return value
class BedrockCredentialsSerializer(serializers.Serializer):
"""
Serializer for AWS Bedrock credentials validation.
Validates long-term AWS credentials (AKIA) and region format.
"""
access_key_id = serializers.CharField()
secret_access_key = serializers.CharField()
region = serializers.CharField()
def validate_access_key_id(self, value: str) -> str:
"""Validate AWS access key ID format (AKIA for long-term credentials)."""
pattern = r"^AKIA[0-9A-Z]{16}$"
if not re.match(pattern, value or ""):
raise serializers.ValidationError(
"Invalid AWS access key ID format. Must be AKIA followed by 16 alphanumeric characters."
)
return value
def validate_secret_access_key(self, value: str) -> str:
"""Validate AWS secret access key format (40 base64 characters)."""
pattern = r"^[A-Za-z0-9/+=]{40}$"
if not re.match(pattern, value or ""):
raise serializers.ValidationError(
"Invalid AWS secret access key format. Must be 40 base64 characters."
)
return value
def validate_region(self, value: str) -> str:
"""Validate AWS region format."""
pattern = r"^[a-z]{2}-[a-z]+-\d+$"
if not re.match(pattern, value or ""):
raise serializers.ValidationError(
"Invalid AWS region format. Expected format like 'us-east-1' or 'eu-west-2'."
)
return value
+32 -1
View File
@@ -59,7 +59,10 @@ from api.v1.serializer_utils.integrations import (
S3ConfigSerializer,
SecurityHubConfigSerializer,
)
from api.v1.serializer_utils.lighthouse import OpenAICredentialsSerializer
from api.v1.serializer_utils.lighthouse import (
BedrockCredentialsSerializer,
OpenAICredentialsSerializer,
)
from api.v1.serializer_utils.processors import ProcessorConfigField
from api.v1.serializer_utils.providers import ProviderSecretField
from prowler.lib.mutelist.mutelist import Mutelist
@@ -3096,6 +3099,19 @@ class LighthouseProviderConfigCreateSerializer(RLSSerializer, BaseWriteSerialize
e.detail[f"credentials/{key}"] = value
del e.detail[key]
raise e
elif (
provider_type == LighthouseProviderConfiguration.LLMProviderChoices.BEDROCK
):
try:
BedrockCredentialsSerializer(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)
@@ -3154,6 +3170,21 @@ class LighthouseProviderConfigUpdateSerializer(BaseWriteSerializer):
e.detail[f"credentials/{key}"] = value
del e.detail[key]
raise e
elif (
credentials is not None
and provider_type
== LighthouseProviderConfiguration.LLMProviderChoices.BEDROCK
):
try:
BedrockCredentialsSerializer(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)
+3 -23
View File
@@ -321,7 +321,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."
)
@@ -4350,23 +4350,13 @@ class LighthouseProviderConfigViewSet(BaseRLSViewSet):
@extend_schema(
tags=["Lighthouse AI"],
summary="Check LLM provider connection",
description="Validate provider credentials asynchronously and toggle is_active.",
description="Validate provider credentials asynchronously and toggle is_active. Supports OpenAI and AWS Bedrock providers.",
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(
@@ -4388,7 +4378,7 @@ class LighthouseProviderConfigViewSet(BaseRLSViewSet):
@extend_schema(
tags=["Lighthouse AI"],
summary="Refresh LLM models catalog",
description="Fetch available models for this provider configuration and upsert into catalog.",
description="Fetch available models for this provider configuration and upsert into catalog. Supports OpenAI and AWS Bedrock providers.",
request=None,
responses={202: OpenApiResponse(response=TaskSerializer)},
)
@@ -4400,16 +4390,6 @@ class LighthouseProviderConfigViewSet(BaseRLSViewSet):
)
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(
@@ -1,6 +1,8 @@
from typing import Dict, Set
from typing import Dict
import boto3
import openai
from botocore.exceptions import BotoCoreError, ClientError
from celery.utils.log import get_task_logger
from api.models import LighthouseProviderConfiguration, LighthouseProviderModels
@@ -30,16 +32,53 @@ def _extract_openai_api_key(
return api_key
def _extract_bedrock_credentials(
provider_cfg: LighthouseProviderConfiguration,
) -> Dict[str, str] | None:
"""
Safely extract AWS Bedrock credentials from a provider configuration.
Args:
provider_cfg (LighthouseProviderConfiguration): The provider configuration instance
containing the credentials.
Returns:
Dict[str, str] | None: Dictionary with 'access_key_id', 'secret_access_key', and
'region' if present and valid, otherwise None.
"""
creds = provider_cfg.credentials_decoded
if not isinstance(creds, dict):
return None
access_key_id = creds.get("access_key_id")
secret_access_key = creds.get("secret_access_key")
region = creds.get("region")
# Validate all required fields are present and are strings
if (
not isinstance(access_key_id, str)
or not access_key_id
or not isinstance(secret_access_key, str)
or not secret_access_key
or not isinstance(region, str)
or not region
):
return None
return {
"access_key_id": access_key_id,
"secret_access_key": secret_access_key,
"region": region,
}
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`
provider_config_id: The primary key of the `LighthouseProviderConfiguration`
to validate.
Returns:
@@ -55,42 +94,192 @@ def check_lighthouse_provider_connection(provider_config_id: str) -> Dict:
"""
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()
if (
provider_cfg.provider_type
== LighthouseProviderConfiguration.LLMProviderChoices.OPENAI
):
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"}
# Test connection by listing models
client = openai.OpenAI(api_key=api_key)
_ = client.models.list()
elif (
provider_cfg.provider_type
== LighthouseProviderConfiguration.LLMProviderChoices.BEDROCK
):
bedrock_creds = _extract_bedrock_credentials(provider_cfg)
if not bedrock_creds:
provider_cfg.is_active = False
provider_cfg.save()
return {
"connected": False,
"error": "AWS credentials are invalid or missing",
}
# Test connection by listing foundation models
bedrock_client = boto3.client(
"bedrock",
aws_access_key_id=bedrock_creds["access_key_id"],
aws_secret_access_key=bedrock_creds["secret_access_key"],
region_name=bedrock_creds["region"],
)
_ = bedrock_client.list_foundation_models()
else:
return {"connected": False, "error": "Unsupported provider type"}
# Connection successful
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))
logger.warning(
"%s connection check failed: %s", provider_cfg.provider_type, str(e)
)
provider_cfg.is_active = False
provider_cfg.save()
return {"connected": False, "error": str(e)}
def _fetch_openai_models(api_key: str) -> Dict[str, str]:
"""
Fetch available models from OpenAI API.
Args:
api_key: OpenAI API key for authentication.
Returns:
Dict mapping model_id to model_name. For OpenAI, both are the same
as the API doesn't provide separate display names.
Raises:
Exception: If the API call fails.
"""
client = openai.OpenAI(api_key=api_key)
models = client.models.list()
# OpenAI uses model.id for both ID and display name
return {m.id: m.id for m in getattr(models, "data", [])}
def _fetch_bedrock_models(bedrock_creds: Dict[str, str]) -> Dict[str, str]:
"""
Fetch available models from AWS Bedrock with entitlement verification.
This function:
1. Lists foundation models with TEXT modality support
2. Lists inference profiles with TEXT modality support
3. Verifies user has entitlement access to each model
Args:
bedrock_creds: Dictionary with 'access_key_id', 'secret_access_key', and 'region'.
Returns:
Dict mapping model_id to model_name for all accessible models.
Raises:
BotoCoreError, ClientError: If AWS API calls fail.
"""
bedrock_client = boto3.client(
"bedrock",
aws_access_key_id=bedrock_creds["access_key_id"],
aws_secret_access_key=bedrock_creds["secret_access_key"],
region_name=bedrock_creds["region"],
)
models_to_check: Dict[str, str] = {}
# Step 1: Get foundation models with TEXT modality
foundation_response = bedrock_client.list_foundation_models()
model_summaries = foundation_response.get("modelSummaries", [])
for model in model_summaries:
# Check if model supports TEXT input and output modality
input_modalities = model.get("inputModalities", [])
output_modalities = model.get("outputModalities", [])
if "TEXT" not in input_modalities or "TEXT" not in output_modalities:
continue
model_id = model.get("modelId")
if not model_id:
continue
inference_types = model.get("inferenceTypesSupported", [])
# Only include models with ON_DEMAND inference support
if "ON_DEMAND" in inference_types:
models_to_check[model_id] = model["modelName"]
# Step 2: Get inference profiles
try:
inference_profiles_response = bedrock_client.list_inference_profiles()
inference_profiles = inference_profiles_response.get(
"inferenceProfileSummaries", []
)
for profile in inference_profiles:
# Check if profile supports TEXT modality
input_modalities = profile.get("inputModalities", [])
output_modalities = profile.get("outputModalities", [])
if "TEXT" not in input_modalities or "TEXT" not in output_modalities:
continue
profile_id = profile.get("inferenceProfileId")
if profile_id:
models_to_check[profile_id] = profile["inferenceProfileName"]
except (BotoCoreError, ClientError) as e:
logger.info(
"Could not fetch inference profiles in %s: %s",
bedrock_creds["region"],
str(e),
)
# Step 3: Verify entitlement availability for each model
available_models: Dict[str, str] = {}
for model_id, model_name in models_to_check.items():
try:
availability = bedrock_client.get_foundation_model_availability(
modelId=model_id
)
entitlement = availability.get("entitlementAvailability")
# Only include models user has access to
if entitlement == "AVAILABLE":
available_models[model_id] = model_name
else:
logger.debug(
"Skipping model %s - entitlement status: %s", model_id, entitlement
)
except (BotoCoreError, ClientError) as e:
logger.debug(
"Could not check availability for model %s: %s", model_id, str(e)
)
continue
return available_models
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.
Fetches the current list of models from the provider, upserts entries into
`LighthouseProviderModels`, and deletes stale entries no longer returned.
Args:
provider_config_id (str): The primary key of the `LighthouseProviderConfiguration`
provider_config_id: The primary key of the `LighthouseProviderConfiguration`
whose models should be refreshed.
Returns:
@@ -104,45 +293,65 @@ def refresh_lighthouse_provider_models(provider_config_id: str) -> Dict:
LighthouseProviderConfiguration.DoesNotExist: If no configuration exists with the given ID.
"""
provider_cfg = LighthouseProviderConfiguration.objects.get(pk=provider_config_id)
fetched_models: Dict[str, str] = {}
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",
}
# Fetch models from the appropriate provider
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))
if (
provider_cfg.provider_type
== LighthouseProviderConfiguration.LLMProviderChoices.OPENAI
):
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",
}
fetched_models = _fetch_openai_models(api_key)
elif (
provider_cfg.provider_type
== LighthouseProviderConfiguration.LLMProviderChoices.BEDROCK
):
bedrock_creds = _extract_bedrock_credentials(provider_cfg)
if not bedrock_creds:
return {
"created": 0,
"updated": 0,
"deleted": 0,
"error": "AWS credentials are invalid or missing",
}
fetched_models = _fetch_bedrock_models(bedrock_creds)
else:
return {
"created": 0,
"updated": 0,
"deleted": 0,
"error": "Unsupported provider type",
}
except Exception as e:
logger.warning(
"Unexpected error refreshing %s models: %s",
provider_cfg.provider_type,
str(e),
)
return {"created": 0, "updated": 0, "deleted": 0, "error": str(e)}
# Upsert models into the catalog
created = 0
updated = 0
for model_id in fetched_ids:
for model_id, model_name in fetched_models.items():
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
"model_name": model_name,
"default_parameters": {},
},
)
@@ -156,7 +365,7 @@ def refresh_lighthouse_provider_models(provider_config_id: str) -> Dict:
LighthouseProviderModels.objects.filter(
tenant_id=provider_cfg.tenant_id, provider_configuration=provider_cfg
)
.exclude(model_id__in=fetched_ids)
.exclude(model_id__in=fetched_models.keys())
.delete()
)
+1 -20
View File
@@ -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.
-12
View File
@@ -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>&nbsp;
<code className="version-badge-version">{version}</code>
</p>
</code>
);
};
-51
View File
@@ -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;
}
@@ -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:
-54
View File
@@ -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.
-203
View File
@@ -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)
-263
View File
@@ -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)
-359
View File
@@ -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)
-346
View File
@@ -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)
-377
View File
@@ -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)
-465
View File
@@ -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)
-425
View File
@@ -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/)
-440
View File
@@ -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)
-366
View File
@@ -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
-16
View File
@@ -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
@@ -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",
+1 -1
View File
@@ -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"
@@ -199,7 +199,6 @@
"aws": [
"ap-south-1",
"ap-southeast-2",
"ca-central-1",
"eu-west-1",
"eu-west-2",
"us-east-1",
@@ -1212,7 +1211,6 @@
"b2bi": {
"regions": {
"aws": [
"eu-west-1",
"us-east-1",
"us-east-2",
"us-west-2"
@@ -1564,7 +1562,6 @@
"ap-southeast-2",
"ap-southeast-3",
"ap-southeast-4",
"ap-southeast-5",
"ca-central-1",
"ca-west-1",
"eu-central-1",
@@ -2943,7 +2940,6 @@
"ap-southeast-3",
"ap-southeast-4",
"ap-southeast-5",
"ap-southeast-6",
"ap-southeast-7",
"ca-central-1",
"ca-west-1",
@@ -3610,7 +3606,6 @@
"ap-northeast-1",
"ap-northeast-2",
"ap-northeast-3",
"eu-central-1",
"eu-west-1",
"eu-west-2",
"eu-west-3",
@@ -5187,7 +5182,6 @@
"aws": [
"af-south-1",
"ap-east-1",
"ap-east-2",
"ap-northeast-1",
"ap-northeast-2",
"ap-northeast-3",
@@ -5198,7 +5192,6 @@
"ap-southeast-3",
"ap-southeast-4",
"ap-southeast-5",
"ap-southeast-7",
"ca-central-1",
"ca-west-1",
"eu-central-1",
@@ -7126,7 +7119,6 @@
"ap-southeast-1",
"ap-southeast-2",
"ap-southeast-3",
"ap-southeast-5",
"ca-central-1",
"eu-central-1",
"eu-north-1",
@@ -7714,7 +7706,6 @@
"ap-southeast-3",
"ap-southeast-4",
"ap-southeast-5",
"ap-southeast-6",
"ap-southeast-7",
"ca-central-1",
"ca-west-1",
@@ -8392,7 +8383,6 @@
"payment-cryptography": {
"regions": {
"aws": [
"af-south-1",
"ap-northeast-1",
"ap-northeast-3",
"ap-south-1",
@@ -1,38 +1,32 @@
{
"Provider": "aws",
"CheckID": "directconnect_connection_redundancy",
"CheckTitle": "Direct Connect connections span at least two locations per region",
"CheckTitle": "Ensure Direct Connect connections are redundant",
"CheckType": [
"Software and Configuration Checks/AWS Security Best Practices",
"Software and Configuration Checks/AWS Security Best Practices/Network Reachability"
"Resilience"
],
"ServiceName": "directconnect",
"SubServiceName": "",
"ResourceIdTemplate": "",
"ResourceIdTemplate": "arn:partition:directconnect:region:account-id:directconnect/resource-id",
"Severity": "medium",
"ResourceType": "Other",
"Description": "**AWS Direct Connect** connectivity is provisioned with **connection and location redundancy**-multiple connections spread across **at least two distinct Direct Connect locations** in each Region.",
"Risk": "Missing **connection/location redundancy** creates a **single point of failure**, degrading **availability**. A router, fiber, or site outage can sever private paths to AWS, stalling app traffic, data replication, and admin access, leading to timeouts or extended downtime until alternate paths are restored.",
"RelatedUrl": "",
"AdditionalURLs": [
"https://docs.aws.amazon.com/awssupport/latest/user/fault-tolerance-checks.html#amazon-direct-connect-location-resiliency",
"https://repost.aws/knowledge-center/direct-connect-physical-redundancy",
"https://aws.amazon.com/directconnect/resiliency-recommendation/"
],
"Description": "Checks the resilience of the AWS Direct Connect used to connect your on-premises.",
"Risk": "This check alerts you if any Direct Connect connections are not redundant and the connections are coming from two distinct Direct Connect locations. Lack of location resiliency can result in unexpected downtime during maintenance, a fiber cut, a device failure, or a complete location failure.",
"RelatedUrl": "https://docs.aws.amazon.com/awssupport/latest/user/fault-tolerance-checks.html#amazon-direct-connect-location-resiliency",
"Remediation": {
"Code": {
"CLI": "aws directconnect create-connection --region <REGION> --location <NEW_DX_LOCATION_CODE> --bandwidth 1Gbps --connection-name <example_resource_name>",
"CLI": "",
"NativeIaC": "",
"Other": "1. In the AWS Console, go to Direct Connect > Connections\n2. Click Create connection\n3. Region: select the Region where the existing connection resides\n4. Name: enter <example_resource_name>\n5. Location: select a different Direct Connect location than your existing connection\n6. Bandwidth: choose a supported value (e.g., 1 Gbps)\n7. Click Create connection",
"Terraform": "```hcl\n# Create an additional Direct Connect connection in a different location\nresource \"aws_dx_connection\" \"example\" {\n name = \"<example_resource_name>\"\n bandwidth = \"1Gbps\"\n location = \"<NEW_DX_LOCATION_CODE>\" # Critical: choose a different DX location in the same Region to achieve location redundancy\n}\n```"
"Other": "",
"Terraform": ""
},
"Recommendation": {
"Text": "Apply **redundancy** and **defense in depth**:\n- Deploy 2 Direct Connect connections across **two distinct locations**\n- Use **dynamic, active/active routing** for automatic failover\n- Ensure **provider/device diversity**\n- Size capacity so one link loss doesn't overload remaining paths\n- Consider a **VPN** as tertiary backup",
"Url": "https://hub.prowler.com/check/directconnect_connection_redundancy"
"Text": "To build Direct Connect location resiliency, you should have at least two connections from at least two distinct Direct Connect locations.",
"Url": "https://aws.amazon.com/directconnect/resiliency-recommendation/"
}
},
"Categories": [
"resilience"
"redundancy"
],
"DependsOn": [],
"RelatedTo": [],
@@ -1,38 +1,32 @@
{
"Provider": "aws",
"CheckID": "directconnect_virtual_interface_redundancy",
"CheckTitle": "Direct Connect gateway or virtual private gateway has at least two virtual interfaces on different Direct Connect connections",
"CheckTitle": "Ensure Direct Connect virtual interface(s) are providing redundant connections",
"CheckType": [
"Software and Configuration Checks/AWS Security Best Practices",
"Effects/Denial of Service"
"Resilience"
],
"ServiceName": "directconnect",
"SubServiceName": "",
"ResourceIdTemplate": "",
"ResourceIdTemplate": "arn:partition:directconnect:region:account-id:directconnect/resource-id",
"Severity": "medium",
"ResourceType": "Other",
"Description": "**Direct Connect gateways** and **virtual private gateways** are assessed for **interface redundancy**: multiple virtual interfaces (`VIFs`) distributed across more than one **Direct Connect connection**.\n\n*Gateways with only one VIF or with all VIFs on a single connection are identified.*",
"Risk": "Missing connection diversity undermines **availability**. A single device, fiber, or location failure can cut on-prem to VPC connectivity, causing **outages**, **packet loss**, or routing blackholes. Fallback to internet VPN can add latency and throttle throughput, delaying recovery and impacting operations.",
"RelatedUrl": "",
"AdditionalURLs": [
"https://docs.aws.amazon.com/awssupport/latest/user/fault-tolerance-checks.html#amazon-direct-connect-location-resiliency",
"https://repost.aws/knowledge-center/direct-connect-physical-redundancy",
"https://aws.amazon.com/directconnect/resiliency-recommendation/"
],
"Description": "Checks the resilience of the AWS Direct Connect used to connect your on-premises to each Direct Connect gateway or virtual private gateway.",
"Risk": "This check alerts you if any Direct Connect gateway or virtual private gateway isn't configured with virtual interfaces across at least two distinct Direct Connect locations. Lack of location resiliency can result in unexpected downtime during maintenance, a fiber cut, a device failure, or a complete location failure.",
"RelatedUrl": "https://docs.aws.amazon.com/awssupport/latest/user/fault-tolerance-checks.html#amazon-direct-connect-location-resiliency",
"Remediation": {
"Code": {
"CLI": "aws directconnect create-private-virtual-interface --connection-id <CONNECTION_ID_DIFFERENT_FROM_EXISTING_VIF> --new-private-virtual-interface '{\"virtualInterfaceName\":\"<NAME>\",\"vlan\":<VLAN>,\"asn\":<BGP_ASN>,\"addressFamily\":\"ipv4\",\"amazonAddress\":\"<AMAZON_IP/30>\",\"customerAddress\":\"<CUSTOMER_IP/30>\",\"directConnectGatewayId\":\"<DIRECT_CONNECT_GATEWAY_ID>\"}'",
"CLI": "",
"NativeIaC": "",
"Other": "1. In the AWS Console, open Direct Connect\n2. Go to Connections and select a different connection than the one used by your existing VIF\n3. Click Create virtual interface and choose Private\n4. For Gateway, select your Direct Connect gateway (or Virtual private gateway for VGW)\n5. Enter VLAN, BGP ASN, and IPv4 peer IPs (Amazon/Customer), then Create\n6. Verify the gateway now has at least two VIFs on different Direct Connect connections",
"Terraform": "```hcl\n# Create a second Private VIF on a different DX connection and attach to the gateway\nresource \"aws_dx_private_virtual_interface\" \"example\" {\n connection_id = \"<example_resource_id>\" # CRITICAL: use a DIFFERENT Direct Connect connection than existing VIFs\n dx_gateway_id = \"<example_resource_id>\" # CRITICAL: attaches the VIF to the Direct Connect gateway (use virtual_gateway_id for VGW)\n name = \"<NAME>\"\n vlan = 100\n bgp_asn = 65000\n address_family = \"ipv4\"\n amazon_address = \"169.254.100.1/30\"\n customer_address = \"169.254.100.2/30\"\n}\n```"
"Other": "",
"Terraform": ""
},
"Recommendation": {
"Text": "Apply connectivity **defense in depth**:\n- Attach at least two `VIFs` per gateway on separate **Direct Connect connections** in distinct locations\n- Prefer active/active dynamic routing and size capacity to survive a link loss\n- *Optionally* add a **VPN/Transit Gateway** path to sustain operations during provider outages",
"Url": "https://hub.prowler.com/check/directconnect_virtual_interface_redundancy"
"Text": "To build Direct Connect location resiliency, you can configure the Direct Connect gateway or virtual private gateway to connect to at least two distinct Direct Connect locations.",
"Url": "https://aws.amazon.com/directconnect/resiliency-recommendation/"
}
},
"Categories": [
"resilience"
"redundancy"
],
"DependsOn": [],
"RelatedTo": [],
@@ -14,37 +14,35 @@ class logging_log_metric_filter_and_alert_for_bucket_permission_changes_enabled(
'resource.type="gcs_bucket" AND protoPayload.methodName="storage.setIamPermissions"'
in metric.filter
):
metric_name = getattr(metric, "name", None) or "unknown"
report = Check_Report_GCP(
metadata=self.metadata(),
resource=metric,
resource_id=metric_name,
project_id=metric.project_id,
location=logging_client.region,
resource_name=(
metric_name if metric_name != "unknown" else "Log Metric Filter"
),
resource_name=metric.name if metric.name else "Log Metric Filter",
)
projects_with_metric.add(metric.project_id)
report.status = "FAIL"
report.status_extended = f"Log metric filter {metric_name} found but no alerts associated in project {metric.project_id}."
report.status_extended = f"Log metric filter {metric.name} found but no alerts associated in project {metric.project_id}."
for alert_policy in monitoring_client.alert_policies:
for filter in alert_policy.filters:
if metric_name in filter:
if metric.name in filter:
report.status = "PASS"
report.status_extended = f"Log metric filter {metric_name} found with alert policy {alert_policy.display_name} associated in project {metric.project_id}."
report.status_extended = f"Log metric filter {metric.name} found with alert policy {alert_policy.display_name} associated in project {metric.project_id}."
break
findings.append(report)
for project in logging_client.project_ids:
if project not in projects_with_metric:
project_obj = logging_client.projects.get(project)
report = Check_Report_GCP(
metadata=self.metadata(),
resource=project_obj,
resource=logging_client.projects[project],
project_id=project,
location=logging_client.region,
resource_name=(getattr(project_obj, "name", None) or "GCP Project"),
resource_name=(
logging_client.projects[project].name
if logging_client.projects[project].name
else "GCP Project"
),
)
report.status = "FAIL"
report.status_extended = f"There are no log metric filters or alerts associated in project {project}."
@@ -14,38 +14,35 @@ class logging_log_metric_filter_and_alert_for_project_ownership_changes_enabled(
'(protoPayload.serviceName="cloudresourcemanager.googleapis.com") AND (ProjectOwnership OR projectOwnerInvitee) OR (protoPayload.serviceData.policyDelta.bindingDeltas.action="REMOVE" AND protoPayload.serviceData.policyDelta.bindingDeltas.role="roles/owner") OR (protoPayload.serviceData.policyDelta.bindingDeltas.action="ADD" AND protoPayload.serviceData.policyDelta.bindingDeltas.role="roles/owner")'
in metric.filter
):
metric_name = getattr(metric, "name", None) or "unknown"
report = Check_Report_GCP(
metadata=self.metadata(),
resource=metric,
resource_id=metric_name,
project_id=metric.project_id,
location=logging_client.region,
resource_name=(
metric_name if metric_name != "unknown" else "Log Metric Filter"
),
resource_name=metric.name if metric.name else "Log Metric Filter",
)
projects_with_metric.add(metric.project_id)
report.status = "FAIL"
report.status_extended = f"Log metric filter {metric_name} found but no alerts associated in project {metric.project_id}."
report.status_extended = f"Log metric filter {metric.name} found but no alerts associated in project {metric.project_id}."
for alert_policy in monitoring_client.alert_policies:
for filter in alert_policy.filters:
if metric_name in filter:
if metric.name in filter:
report.status = "PASS"
report.status_extended = f"Log metric filter {metric_name} found with alert policy {alert_policy.display_name} associated in project {metric.project_id}."
report.status_extended = f"Log metric filter {metric.name} found with alert policy {alert_policy.display_name} associated in project {metric.project_id}."
break
findings.append(report)
for project in logging_client.project_ids:
if project not in projects_with_metric:
project_obj = logging_client.projects.get(project)
report = Check_Report_GCP(
metadata=self.metadata(),
resource=project_obj,
resource_id=project,
resource=logging_client.projects[project],
project_id=project,
location=logging_client.region,
resource_name=(getattr(project_obj, "name", None) or "GCP Project"),
resource_name=(
logging_client.projects[project].name
if logging_client.projects[project].name
else "GCP Project"
),
)
report.status = "FAIL"
report.status_extended = f"There are no log metric filters or alerts associated in project {project}."
@@ -12,32 +12,32 @@ class logging_sink_created(Check):
for project in logging_client.project_ids:
if project not in projects_with_logging_sink.keys():
project_obj = logging_client.projects.get(project)
report = Check_Report_GCP(
metadata=self.metadata(),
resource=project_obj,
resource_id=project,
resource=logging_client.projects[project],
project_id=project,
location=logging_client.region,
resource_name=(getattr(project_obj, "name", None) or "GCP Project"),
resource_name=(
logging_client.projects[project].name
if logging_client.projects[project].name
else "GCP Project"
),
)
report.status = "FAIL"
report.status_extended = f"There are no logging sinks to export copies of all the log entries in project {project}."
findings.append(report)
else:
sink = projects_with_logging_sink[project]
sink_name = getattr(sink, "name", None) or "unknown"
report = Check_Report_GCP(
metadata=self.metadata(),
resource=sink,
resource_id=sink_name,
project_id=project,
resource=projects_with_logging_sink[project],
location=logging_client.region,
resource_name=(
sink_name if sink_name != "unknown" else "Logging Sink"
projects_with_logging_sink[project].name
if projects_with_logging_sink[project].name
else "Logging Sink"
),
)
report.status = "PASS"
report.status_extended = f"Sink {sink_name} is enabled exporting copies of all the log entries in project {project}."
report.status_extended = f"Sink {projects_with_logging_sink[project].name} is enabled exporting copies of all the log entries in project {project}."
findings.append(report)
return findings
@@ -1,32 +0,0 @@
{
"Provider": "github",
"CheckID": "organization_default_repository_permission_strict",
"CheckTitle": "Ensure strict base repository permissions are set for the organization",
"CheckType": [],
"ServiceName": "organization",
"SubServiceName": "",
"ResourceIdTemplate": "",
"Severity": "high",
"ResourceType": "GitHubOrganization",
"Description": "Ensure the organization's base repository permission for members is set to 'read' or 'none' to minimize risk.",
"Risk": "If base repository permissions allow 'write' or 'admin' by default, organization members may unintentionally gain excessive privileges across repositories, increasing the risk of unauthorized changes or accidental modifications.",
"RelatedUrl": "https://docs.github.com/en/organizations/managing-user-access-to-your-organizations-repositories/managing-repository-roles/setting-base-permissions-for-an-organization",
"Remediation": {
"Code": {
"CLI": "",
"NativeIaC": "",
"Other": "",
"Terraform": ""
},
"Recommendation": {
"Text": "Set the organization's base repository permission to 'read' or 'none' for members, unless stricter requirements are needed.",
"Url": "https://docs.github.com/en/organizations/managing-user-access-to-your-organizations-repositories/managing-repository-roles/setting-base-permissions-for-an-organization"
}
},
"AdditionalURLs": [],
"Categories": [],
"DependsOn": [],
"RelatedTo": [],
"Notes": ""
}
@@ -1,36 +0,0 @@
from typing import List
from prowler.lib.check.models import Check, CheckReportGithub
from prowler.providers.github.services.organization.organization_client import (
organization_client,
)
class organization_default_repository_permission_strict(Check):
"""Check if an organization's base repository permission is set to a strict level.
PASS: base permission is "read" or "none"
FAIL: base permission is "write" or "admin" (or any other non-strict value)
"""
def execute(self) -> List[CheckReportGithub]:
findings = []
for org in organization_client.organizations.values():
base_perm = getattr(org, "base_permission", None)
if base_perm is None:
# Unknown / no permission to read → skip producing a finding
continue
p = str(base_perm).lower()
report = CheckReportGithub(metadata=self.metadata(), resource=org)
if p in ("read", "none"):
report.status = "PASS"
report.status_extended = f"Organization {org.name} base repository permission is '{p}', which is strict."
else:
report.status = "FAIL"
report.status_extended = f"Organization {org.name} base repository permission is '{p}', which is not strict."
findings.append(report)
return findings
@@ -5,7 +5,7 @@ from pydantic.v1 import BaseModel
from prowler.lib.logger import logger
from prowler.providers.github.lib.service.service import GithubService
from prowler.providers.github.models import GithubAppIdentityInfo
from prowler.providers.github.models import GithubAppIdentityInfo, GithubIdentityInfo
class Organization(GithubService):
@@ -38,15 +38,13 @@ class Organization(GithubService):
org_names_to_check = set()
try:
for client in getattr(self, "clients", []) or []:
if getattr(self.provider, "organizations", None):
for client in self.clients:
if self.provider.organizations:
org_names_to_check.update(self.provider.organizations)
# If repositories are specified without organizations, don't perform organization checks
# Only add repository owners to organization checks if organizations are also specified
if getattr(self.provider, "repositories", None) and getattr(
self.provider, "organizations", None
):
if self.provider.repositories and self.provider.organizations:
for repo_name in self.provider.repositories:
if "/" in repo_name:
owner_name = repo_name.split("/")[0]
@@ -113,19 +111,18 @@ class Organization(GithubService):
logger.error(
f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
)
elif not getattr(self.provider, "repositories", None):
elif not self.provider.repositories:
# Default behavior: get all organizations the user is a member of
# Only when no repositories are specified
if isinstance(self.provider.identity, GithubAppIdentityInfo):
orgs = client.get_organizations()
if getattr(orgs, "totalCount", 0) > 0:
for org in orgs:
self._process_organization(org, organizations)
else:
# Default (personal access/OAuth): use user organizations
if isinstance(self.provider.identity, GithubIdentityInfo):
orgs = client.get_user().get_orgs()
for org in orgs:
self._process_organization(org, organizations)
elif isinstance(self.provider.identity, GithubAppIdentityInfo):
orgs = client.get_organizations()
if orgs.totalCount > 0:
for org in orgs:
self._process_organization(org, organizations)
except github.RateLimitExceededException as error:
logger.error(f"GitHub API rate limit exceeded: {error}")
@@ -147,22 +144,10 @@ class Organization(GithubService):
logger.error(
f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
)
# Base permission (default repository permission for members)
base_perm: Optional[str] = None
try:
base_perm = getattr(org, "default_repository_permission", None)
except Exception as error:
logger.error(
f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
)
base_perm = None
organizations[org.id] = Org(
id=org.id,
name=org.login,
mfa_required=require_mfa,
base_permission=base_perm,
)
@@ -172,4 +157,3 @@ class Org(BaseModel):
id: int
name: str
mfa_required: Optional[bool] = False
base_permission: Optional[str] = None
+1 -1
View File
@@ -76,7 +76,7 @@ maintainers = [{name = "Prowler Engineering", email = "engineering@prowler.com"}
name = "prowler"
readme = "README.md"
requires-python = ">3.9.1,<3.13"
version = "5.14.0"
version = "5.13.0"
[project.scripts]
prowler = "prowler.__main__:prowler"
@@ -259,141 +259,3 @@ class Test_logging_log_metric_filter_and_alert_for_bucket_permission_changes_ena
assert result[0].resource_name == "metric_name"
assert result[0].project_id == GCP_PROJECT_ID
assert result[0].location == GCP_EU1_LOCATION
def test_log_metric_filters_with_none_name(self):
"""Test that metric with None name uses fallback 'Log Metric Filter'"""
logging_client = MagicMock()
monitoring_client = MagicMock()
with (
patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=set_mocked_gcp_provider(),
),
patch(
"prowler.providers.gcp.services.logging.logging_log_metric_filter_and_alert_for_bucket_permission_changes_enabled.logging_log_metric_filter_and_alert_for_bucket_permission_changes_enabled.logging_client",
new=logging_client,
),
patch(
"prowler.providers.gcp.services.logging.logging_log_metric_filter_and_alert_for_bucket_permission_changes_enabled.logging_log_metric_filter_and_alert_for_bucket_permission_changes_enabled.monitoring_client",
new=monitoring_client,
),
):
from prowler.providers.gcp.services.logging.logging_log_metric_filter_and_alert_for_bucket_permission_changes_enabled.logging_log_metric_filter_and_alert_for_bucket_permission_changes_enabled import (
logging_log_metric_filter_and_alert_for_bucket_permission_changes_enabled,
)
# Create a MagicMock metric object with name=None
metric = MagicMock()
metric.name = None
metric.filter = 'resource.type="gcs_bucket" AND protoPayload.methodName="storage.setIamPermissions"'
metric.project_id = GCP_PROJECT_ID
logging_client.metrics = [metric]
logging_client.project_ids = [GCP_PROJECT_ID]
logging_client.region = GCP_EU1_LOCATION
monitoring_client.alert_policies = []
check = (
logging_log_metric_filter_and_alert_for_bucket_permission_changes_enabled()
)
result = check.execute()
assert len(result) == 1
assert result[0].status == "FAIL"
assert result[0].resource_name == "Log Metric Filter"
assert (
result[0].resource_id == "unknown"
) # resource_id should never be None
assert result[0].project_id == GCP_PROJECT_ID
assert result[0].location == GCP_EU1_LOCATION
# When name is None, the 'or' pattern makes it use "unknown"
assert "unknown" in result[0].status_extended
def test_log_metric_filters_with_missing_name_attribute(self):
"""Test that metric without name attribute uses fallback 'Log Metric Filter'"""
logging_client = MagicMock()
monitoring_client = MagicMock()
with (
patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=set_mocked_gcp_provider(),
),
patch(
"prowler.providers.gcp.services.logging.logging_log_metric_filter_and_alert_for_bucket_permission_changes_enabled.logging_log_metric_filter_and_alert_for_bucket_permission_changes_enabled.logging_client",
new=logging_client,
),
patch(
"prowler.providers.gcp.services.logging.logging_log_metric_filter_and_alert_for_bucket_permission_changes_enabled.logging_log_metric_filter_and_alert_for_bucket_permission_changes_enabled.monitoring_client",
new=monitoring_client,
),
):
from prowler.providers.gcp.services.logging.logging_log_metric_filter_and_alert_for_bucket_permission_changes_enabled.logging_log_metric_filter_and_alert_for_bucket_permission_changes_enabled import (
logging_log_metric_filter_and_alert_for_bucket_permission_changes_enabled,
)
# Create a MagicMock metric object without name attribute
metric = MagicMock(spec=["filter", "project_id"])
metric.filter = 'resource.type="gcs_bucket" AND protoPayload.methodName="storage.setIamPermissions"'
metric.project_id = GCP_PROJECT_ID
logging_client.metrics = [metric]
logging_client.project_ids = [GCP_PROJECT_ID]
logging_client.region = GCP_EU1_LOCATION
monitoring_client.alert_policies = []
check = (
logging_log_metric_filter_and_alert_for_bucket_permission_changes_enabled()
)
result = check.execute()
assert len(result) == 1
assert result[0].status == "FAIL"
assert result[0].resource_name == "Log Metric Filter"
assert (
result[0].resource_id == "unknown"
) # resource_id should never be None
assert result[0].project_id == GCP_PROJECT_ID
assert result[0].location == GCP_EU1_LOCATION
def test_project_not_in_projects_dict(self):
"""Test that project not in projects dict uses None and fallback name"""
logging_client = MagicMock()
monitoring_client = MagicMock()
with (
patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=set_mocked_gcp_provider(),
),
patch(
"prowler.providers.gcp.services.logging.logging_log_metric_filter_and_alert_for_bucket_permission_changes_enabled.logging_log_metric_filter_and_alert_for_bucket_permission_changes_enabled.logging_client",
new=logging_client,
),
patch(
"prowler.providers.gcp.services.logging.logging_log_metric_filter_and_alert_for_bucket_permission_changes_enabled.logging_log_metric_filter_and_alert_for_bucket_permission_changes_enabled.monitoring_client",
new=monitoring_client,
),
):
from prowler.providers.gcp.services.logging.logging_log_metric_filter_and_alert_for_bucket_permission_changes_enabled.logging_log_metric_filter_and_alert_for_bucket_permission_changes_enabled import (
logging_log_metric_filter_and_alert_for_bucket_permission_changes_enabled,
)
logging_client.metrics = []
logging_client.project_ids = [GCP_PROJECT_ID]
logging_client.region = GCP_EU1_LOCATION
# Project is in project_ids but NOT in projects dict
logging_client.projects = {}
monitoring_client.alert_policies = []
check = (
logging_log_metric_filter_and_alert_for_bucket_permission_changes_enabled()
)
result = check.execute()
assert len(result) == 1
assert result[0].status == "FAIL"
assert result[0].resource_name == "GCP Project"
assert result[0].project_id == GCP_PROJECT_ID
assert result[0].location == GCP_EU1_LOCATION
@@ -259,136 +259,3 @@ class Test_logging_log_metric_filter_and_alert_for_project_ownership_changes_ena
assert result[0].resource_name == "metric_name"
assert result[0].project_id == GCP_PROJECT_ID
assert result[0].location == GCP_EU1_LOCATION
def test_log_metric_filters_with_none_name(self):
"""Test that metric with None name uses fallback 'Log Metric Filter'"""
logging_client = MagicMock()
monitoring_client = MagicMock()
with (
patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=set_mocked_gcp_provider(),
),
patch(
"prowler.providers.gcp.services.logging.logging_log_metric_filter_and_alert_for_project_ownership_changes_enabled.logging_log_metric_filter_and_alert_for_project_ownership_changes_enabled.logging_client",
new=logging_client,
),
patch(
"prowler.providers.gcp.services.logging.logging_log_metric_filter_and_alert_for_project_ownership_changes_enabled.logging_log_metric_filter_and_alert_for_project_ownership_changes_enabled.monitoring_client",
new=monitoring_client,
),
):
from prowler.providers.gcp.services.logging.logging_log_metric_filter_and_alert_for_project_ownership_changes_enabled.logging_log_metric_filter_and_alert_for_project_ownership_changes_enabled import (
logging_log_metric_filter_and_alert_for_project_ownership_changes_enabled,
)
# Create a MagicMock metric object with name=None
metric = MagicMock()
metric.name = None
metric.filter = '(protoPayload.serviceName="cloudresourcemanager.googleapis.com") AND (ProjectOwnership OR projectOwnerInvitee) OR (protoPayload.serviceData.policyDelta.bindingDeltas.action="REMOVE" AND protoPayload.serviceData.policyDelta.bindingDeltas.role="roles/owner") OR (protoPayload.serviceData.policyDelta.bindingDeltas.action="ADD" AND protoPayload.serviceData.policyDelta.bindingDeltas.role="roles/owner")'
metric.project_id = GCP_PROJECT_ID
logging_client.metrics = [metric]
logging_client.project_ids = [GCP_PROJECT_ID]
logging_client.region = GCP_EU1_LOCATION
monitoring_client.alert_policies = []
check = (
logging_log_metric_filter_and_alert_for_project_ownership_changes_enabled()
)
result = check.execute()
assert len(result) == 1
assert result[0].status == "FAIL"
assert result[0].resource_name == "Log Metric Filter"
assert result[0].resource_id == "unknown"
assert result[0].project_id == GCP_PROJECT_ID
assert result[0].location == GCP_EU1_LOCATION
assert "unknown" in result[0].status_extended
def test_log_metric_filters_with_missing_name_attribute(self):
"""Test that metric without name attribute uses fallback 'Log Metric Filter'"""
logging_client = MagicMock()
monitoring_client = MagicMock()
with (
patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=set_mocked_gcp_provider(),
),
patch(
"prowler.providers.gcp.services.logging.logging_log_metric_filter_and_alert_for_project_ownership_changes_enabled.logging_log_metric_filter_and_alert_for_project_ownership_changes_enabled.logging_client",
new=logging_client,
),
patch(
"prowler.providers.gcp.services.logging.logging_log_metric_filter_and_alert_for_project_ownership_changes_enabled.logging_log_metric_filter_and_alert_for_project_ownership_changes_enabled.monitoring_client",
new=monitoring_client,
),
):
from prowler.providers.gcp.services.logging.logging_log_metric_filter_and_alert_for_project_ownership_changes_enabled.logging_log_metric_filter_and_alert_for_project_ownership_changes_enabled import (
logging_log_metric_filter_and_alert_for_project_ownership_changes_enabled,
)
# Create a MagicMock metric object without name attribute
metric = MagicMock(spec=["filter", "project_id"])
metric.filter = '(protoPayload.serviceName="cloudresourcemanager.googleapis.com") AND (ProjectOwnership OR projectOwnerInvitee) OR (protoPayload.serviceData.policyDelta.bindingDeltas.action="REMOVE" AND protoPayload.serviceData.policyDelta.bindingDeltas.role="roles/owner") OR (protoPayload.serviceData.policyDelta.bindingDeltas.action="ADD" AND protoPayload.serviceData.policyDelta.bindingDeltas.role="roles/owner")'
metric.project_id = GCP_PROJECT_ID
logging_client.metrics = [metric]
logging_client.project_ids = [GCP_PROJECT_ID]
logging_client.region = GCP_EU1_LOCATION
monitoring_client.alert_policies = []
check = (
logging_log_metric_filter_and_alert_for_project_ownership_changes_enabled()
)
result = check.execute()
assert len(result) == 1
assert result[0].status == "FAIL"
assert result[0].resource_name == "Log Metric Filter"
assert result[0].resource_id == "unknown"
assert result[0].project_id == GCP_PROJECT_ID
assert result[0].location == GCP_EU1_LOCATION
def test_project_not_in_projects_dict(self):
"""Test that project not in projects dict uses None and fallback name"""
logging_client = MagicMock()
monitoring_client = MagicMock()
with (
patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=set_mocked_gcp_provider(),
),
patch(
"prowler.providers.gcp.services.logging.logging_log_metric_filter_and_alert_for_project_ownership_changes_enabled.logging_log_metric_filter_and_alert_for_project_ownership_changes_enabled.logging_client",
new=logging_client,
),
patch(
"prowler.providers.gcp.services.logging.logging_log_metric_filter_and_alert_for_project_ownership_changes_enabled.logging_log_metric_filter_and_alert_for_project_ownership_changes_enabled.monitoring_client",
new=monitoring_client,
),
):
from prowler.providers.gcp.services.logging.logging_log_metric_filter_and_alert_for_project_ownership_changes_enabled.logging_log_metric_filter_and_alert_for_project_ownership_changes_enabled import (
logging_log_metric_filter_and_alert_for_project_ownership_changes_enabled,
)
logging_client.metrics = []
logging_client.project_ids = [GCP_PROJECT_ID]
logging_client.region = GCP_EU1_LOCATION
# Project is in project_ids but NOT in projects dict
logging_client.projects = {}
monitoring_client.alert_policies = []
check = (
logging_log_metric_filter_and_alert_for_project_ownership_changes_enabled()
)
result = check.execute()
assert len(result) == 1
assert result[0].status == "FAIL"
assert result[0].resource_name == "GCP Project"
assert result[0].project_id == GCP_PROJECT_ID
assert result[0].location == GCP_EU1_LOCATION
@@ -211,128 +211,3 @@ class Test_logging_sink_created:
result[0].status_extended
== f"There are no logging sinks to export copies of all the log entries in project {GCP_PROJECT_ID}."
)
def test_project_not_in_projects_dict(self):
"""Test that project not in projects dict uses None and fallback name"""
logging_client = MagicMock()
with (
patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=set_mocked_gcp_provider(),
),
patch(
"prowler.providers.gcp.services.logging.logging_sink_created.logging_sink_created.logging_client",
new=logging_client,
),
):
from prowler.providers.gcp.services.logging.logging_sink_created.logging_sink_created import (
logging_sink_created,
)
logging_client.project_ids = [GCP_PROJECT_ID]
logging_client.region = GCP_EU1_LOCATION
logging_client.sinks = []
# Project is in project_ids but NOT in projects dict
logging_client.projects = {}
check = logging_sink_created()
result = check.execute()
assert len(result) == 1
assert result[0].status == "FAIL"
assert result[0].resource_name == "GCP Project"
assert result[0].resource_id == GCP_PROJECT_ID
assert result[0].project_id == GCP_PROJECT_ID
assert result[0].location == GCP_EU1_LOCATION
def test_sink_with_none_name(self):
"""Test that sink with None name uses fallback 'Logging Sink'"""
logging_client = MagicMock()
with (
patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=set_mocked_gcp_provider(),
),
patch(
"prowler.providers.gcp.services.logging.logging_sink_created.logging_sink_created.logging_client",
new=logging_client,
),
):
from prowler.providers.gcp.services.logging.logging_sink_created.logging_sink_created import (
logging_sink_created,
)
# Create a MagicMock sink object with name=None
sink = MagicMock()
sink.name = None
sink.filter = "all"
sink.project_id = GCP_PROJECT_ID
logging_client.project_ids = [GCP_PROJECT_ID]
logging_client.region = GCP_EU1_LOCATION
logging_client.sinks = [sink]
logging_client.projects = {
GCP_PROJECT_ID: GCPProject(
id=GCP_PROJECT_ID,
number="123456789012",
name="test",
labels={},
lifecycle_state="ACTIVE",
)
}
check = logging_sink_created()
result = check.execute()
assert len(result) == 1
assert result[0].status == "PASS"
assert result[0].resource_name == "Logging Sink"
assert result[0].resource_id == "unknown"
assert result[0].project_id == GCP_PROJECT_ID
assert result[0].location == GCP_EU1_LOCATION
assert "unknown" in result[0].status_extended
def test_sink_with_missing_name_attribute(self):
"""Test that sink without name attribute uses fallback 'Logging Sink'"""
logging_client = MagicMock()
with (
patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=set_mocked_gcp_provider(),
),
patch(
"prowler.providers.gcp.services.logging.logging_sink_created.logging_sink_created.logging_client",
new=logging_client,
),
):
from prowler.providers.gcp.services.logging.logging_sink_created.logging_sink_created import (
logging_sink_created,
)
# Create a MagicMock sink object without name attribute
sink = MagicMock(spec=["filter", "project_id"])
sink.filter = "all"
sink.project_id = GCP_PROJECT_ID
logging_client.project_ids = [GCP_PROJECT_ID]
logging_client.region = GCP_EU1_LOCATION
logging_client.sinks = [sink]
logging_client.projects = {
GCP_PROJECT_ID: GCPProject(
id=GCP_PROJECT_ID,
number="123456789012",
name="test",
labels={},
lifecycle_state="ACTIVE",
)
}
check = logging_sink_created()
result = check.execute()
assert len(result) == 1
assert result[0].status == "PASS"
assert result[0].resource_name == "Logging Sink"
assert result[0].resource_id == "unknown"
assert result[0].project_id == GCP_PROJECT_ID
assert result[0].location == GCP_EU1_LOCATION
@@ -1,201 +0,0 @@
from unittest import mock
from prowler.providers.github.services.organization.organization_service import Org
from tests.providers.github.github_fixtures import set_mocked_github_provider
class Test_organization_default_repository_permission_strict:
def test_no_organizations(self):
organization_client = mock.MagicMock
organization_client.organizations = {}
with (
mock.patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=set_mocked_github_provider(),
),
mock.patch(
"prowler.providers.github.services.organization.organization_default_repository_permission_strict.organization_default_repository_permission_strict.organization_client",
new=organization_client,
),
):
from prowler.providers.github.services.organization.organization_default_repository_permission_strict.organization_default_repository_permission_strict import (
organization_default_repository_permission_strict,
)
check = organization_default_repository_permission_strict()
result = check.execute()
assert len(result) == 0
def test_permission_read(self):
organization_client = mock.MagicMock
org_name = "test-organization"
organization_client.organizations = {
1: Org(
id=1,
name=org_name,
base_permission="read",
),
}
with (
mock.patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=set_mocked_github_provider(),
),
mock.patch(
"prowler.providers.github.services.organization.organization_default_repository_permission_strict.organization_default_repository_permission_strict.organization_client",
new=organization_client,
),
):
from prowler.providers.github.services.organization.organization_default_repository_permission_strict.organization_default_repository_permission_strict import (
organization_default_repository_permission_strict,
)
check = organization_default_repository_permission_strict()
result = check.execute()
assert len(result) == 1
assert result[0].resource_id == 1
assert result[0].resource_name == org_name
assert result[0].status == "PASS"
assert (
result[0].status_extended
== f"Organization {org_name} base repository permission is 'read', which is strict."
)
def test_permission_none(self):
organization_client = mock.MagicMock
org_name = "test-organization"
organization_client.organizations = {
1: Org(
id=1,
name=org_name,
base_permission="none",
),
}
with (
mock.patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=set_mocked_github_provider(),
),
mock.patch(
"prowler.providers.github.services.organization.organization_default_repository_permission_strict.organization_default_repository_permission_strict.organization_client",
new=organization_client,
),
):
from prowler.providers.github.services.organization.organization_default_repository_permission_strict.organization_default_repository_permission_strict import (
organization_default_repository_permission_strict,
)
check = organization_default_repository_permission_strict()
result = check.execute()
assert len(result) == 1
assert result[0].resource_id == 1
assert result[0].resource_name == org_name
assert result[0].status == "PASS"
assert (
result[0].status_extended
== f"Organization {org_name} base repository permission is 'none', which is strict."
)
def test_permission_write(self):
organization_client = mock.MagicMock
org_name = "test-organization"
organization_client.organizations = {
1: Org(
id=1,
name=org_name,
base_permission="write",
),
}
with (
mock.patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=set_mocked_github_provider(),
),
mock.patch(
"prowler.providers.github.services.organization.organization_default_repository_permission_strict.organization_default_repository_permission_strict.organization_client",
new=organization_client,
),
):
from prowler.providers.github.services.organization.organization_default_repository_permission_strict.organization_default_repository_permission_strict import (
organization_default_repository_permission_strict,
)
check = organization_default_repository_permission_strict()
result = check.execute()
assert len(result) == 1
assert result[0].resource_id == 1
assert result[0].resource_name == org_name
assert result[0].status == "FAIL"
assert (
result[0].status_extended
== f"Organization {org_name} base repository permission is 'write', which is not strict."
)
def test_permission_admin(self):
organization_client = mock.MagicMock
org_name = "test-organization"
organization_client.organizations = {
1: Org(
id=1,
name=org_name,
base_permission="admin",
),
}
with (
mock.patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=set_mocked_github_provider(),
),
mock.patch(
"prowler.providers.github.services.organization.organization_default_repository_permission_strict.organization_default_repository_permission_strict.organization_client",
new=organization_client,
),
):
from prowler.providers.github.services.organization.organization_default_repository_permission_strict.organization_default_repository_permission_strict import (
organization_default_repository_permission_strict,
)
check = organization_default_repository_permission_strict()
result = check.execute()
assert len(result) == 1
assert result[0].resource_id == 1
assert result[0].resource_name == org_name
assert result[0].status == "FAIL"
assert (
result[0].status_extended
== f"Organization {org_name} base repository permission is 'admin', which is not strict."
)
def test_permission_unknown_none_skipped(self):
organization_client = mock.MagicMock
org_name = "test-organization"
organization_client.organizations = {
1: Org(
id=1,
name=org_name,
base_permission=None,
),
}
with (
mock.patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=set_mocked_github_provider(),
),
mock.patch(
"prowler.providers.github.services.organization.organization_default_repository_permission_strict.organization_default_repository_permission_strict.organization_client",
new=organization_client,
),
):
from prowler.providers.github.services.organization.organization_default_repository_permission_strict.organization_default_repository_permission_strict import (
organization_default_repository_permission_strict,
)
check = organization_default_repository_permission_strict()
result = check.execute()
assert len(result) == 0
@@ -45,13 +45,11 @@ class Test_Organization_Scoping:
self.mock_org1.id = 1
self.mock_org1.login = "test-org1"
self.mock_org1.two_factor_requirement_enabled = True
self.mock_org1.default_repository_permission = None
self.mock_org2 = MagicMock()
self.mock_org2.id = 2
self.mock_org2.login = "test-org2"
self.mock_org2.two_factor_requirement_enabled = False
self.mock_org2.default_repository_permission = None
self.mock_user = MagicMock()
self.mock_user.id = 100
@@ -177,35 +175,6 @@ class Test_Organization_Scoping:
assert len(orgs) == 0
def test_base_permission_extraction(self):
"""Test that base_permission is populated from organization's default_repository_permission"""
provider = set_mocked_github_provider()
provider.repositories = []
provider.organizations = ["test-org1"]
mock_client = MagicMock()
# Organization with default_repository_permission set to "read"
org_with_perm = MagicMock()
org_with_perm.id = 1
org_with_perm.login = "test-org1"
org_with_perm.two_factor_requirement_enabled = True
org_with_perm.default_repository_permission = "read"
mock_client.get_organization.return_value = org_with_perm
with patch(
"prowler.providers.github.services.organization.organization_service.GithubService.__init__"
):
organization_service = Organization(provider)
organization_service.clients = [mock_client]
organization_service.provider = provider
orgs = organization_service._list_organizations()
assert len(orgs) == 1
assert 1 in orgs
assert orgs[1].name == "test-org1"
assert orgs[1].base_permission == "read"
def test_specific_organization_scoping(self):
"""Test that only specified organizations are returned"""
provider = set_mocked_github_provider()
@@ -318,13 +287,11 @@ class Test_Organization_Scoping:
mock_owner_org.id = 1
mock_owner_org.login = "owner1"
mock_owner_org.two_factor_requirement_enabled = True
mock_owner_org.default_repository_permission = None
mock_specific_org = MagicMock()
mock_specific_org.id = 2
mock_specific_org.login = "specific-org"
mock_specific_org.two_factor_requirement_enabled = False
mock_specific_org.default_repository_permission = None
mock_client.get_organization.side_effect = [
mock_owner_org,
@@ -426,7 +393,6 @@ class Test_Organization_ErrorHandling:
self.mock_org1.id = 1
self.mock_org1.login = "test-org1"
self.mock_org1.two_factor_requirement_enabled = True
self.mock_org1.default_repository_permission = None
def test_github_api_error_handling(self):
"""Test that GitHub API errors are handled properly"""
+1 -412
View File
@@ -390,7 +390,6 @@ ui/
├── types/ # TypeScript type definitions
├── hooks/ # Custom React hooks
├── store/ # Zustand state management
├── tests/ # Playwright E2E tests
└── styles/ # Global CSS & Tailwind config
```
@@ -684,423 +683,13 @@ const text = message.parts
### Playwright E2E Tests
**⚠️ MANDATORY: If you have access to Playwright MCP tools, ALWAYS use them to understand the actual application flow before creating any E2E test.**
- **IF Playwright MCP is available**: Use browser tools to navigate, interact, and understand the real UI behavior FIRST, then create tests
- **IF Playwright MCP is NOT available**: Proceed with test creation based on available documentation and code analysis
- Add/update E2E tests for critical flows you modify
- Scope: run only affected specs when iterating
- Commit snapshot updates only with real UI changes
- Determinism: avoid relying on real external services; mock or stub where possible
- **Organization**: Create a folder under `tests/` for each page (e.g., `tests/sign-in/`, `tests/sign-up/`, etc.)
- **File Structure**: Each page folder should contain 3 files:
- `{page-name}-page.ts` - Page Object Model
- `{page-name}.spec.ts` - Test specifications
- `{page-name}.md` - Test documentation
- **Base Class**: `tests/base-page.ts` - Parent class that all `{page-name}-page.ts` files should extend
- **Helpers**: `tests/helpers.ts` - Utility functions and helper methods for tests
#### Playwright MCP Integration
**⚠️ CRITICAL WORKFLOW (When Available): If you have access to Playwright MCP browser tools, use them to explore the application BEFORE writing any test code.**
**Recommended Steps Before Creating Tests (Only if MCP Tools are Available):**
1. **Navigate to the application** to reach the target page
2. **Take a snapshot** to see the page structure and available elements
3. **Interact with forms and elements** to verify the exact user flow
4. **Take screenshots** to document expected states at each step
5. **Verify page transitions** by navigating through the complete flow to understand all states (loading, success, error)
6. **Document actual selectors** from the snapshots - use the real element references (ref) and labels you observe
7. **Only after exploring** the complete flow manually, create the test code with the exact selectors and steps you verified
**Why This Matters (When MCP Tools are Available):**
- ✅ **Precise test creation** - Only include the exact steps needed, no assumptions or guessing
- ✅ **Accurate selectors** - Use the actual DOM structure from real snapshots, not imagined selectors
- ✅ **Real flow validation** - Verify the complete user journey actually works as expected
- ✅ **Avoid over-engineering** - Create minimal tests that focus on what actually exists
- ✅ **Prevent flaky tests** - Tests based on real exploration are more stable and reliable
- ❌ **Never assume** - Don't create tests based on assumptions about how the UI "should" work
**Benefits:**
- **Precise test creation** - Only include the exact steps needed for the test requirement
- **Accurate selectors** - Use the actual DOM structure to create reliable locators
- **Real flow validation** - Verify the complete user journey works as expected
- **Avoid over-engineering** - Create minimal tests that focus on the specific requirement
#### Test Creation Guidelines
**IMPORTANT: Always ask for clarification if the request is ambiguous about scope.**
**When creating a specific test:**
- Create only a single `test()` entry implementing the specific functionality described
- Do NOT create the full test suite for this page
- **ALWAYS add the test to the page's main spec file** (e.g., `sign-up.spec.ts`), NOT in a separate file
- **REUSE existing page objects** from other pages when possible (e.g., use existing SignInPage, HomePage, etc.)
- If the page's spec file doesn't exist, create minimal structure:
- `{page-name}-page.ts` - Page Object Model
- `{page-name}.spec.ts` - Test specifications (add your specific test here)
- Focus on the exact requirement without additional test cases
- Do NOT create separate files like `{page-name}-critical-path.spec.ts` or `{page-name}-specific-test.spec.ts`
**When creating comprehensive page tests:**
- Create the full test suite with all files (page object, spec, documentation)
- Include multiple test cases covering various scenarios in `{page-name}.spec.ts`
- Follow the complete structure with validation, error handling, accessibility tests
- Create comprehensive documentation for all test cases in `{page-name}.md`
**File Naming Convention:**
- ✅ **CORRECT**: `sign-up.spec.ts` (contains all sign-up tests)
- ✅ **CORRECT**: `sign-up-page.ts` (page object)
- ✅ **CORRECT**: `sign-up.md` (documentation for all tests)
- ❌ **WRONG**: `sign-up-critical-path.spec.ts` (separate file for specific test)
- ❌ **WRONG**: `sign-up-validation.spec.ts` (separate file for specific test)
**Examples:**
```typescript
// ✅ Specific test request - create only this test
test("User can create account and login successfully",{
tag: ['@critical', '@e2e', '@signup', '@SIGNUP-E2E-001']
} async ({ page }) => {
// Implementation for this specific test only
});
// ❌ Don't create full suite when only one test is requested
```
**Request Examples:**
- **"Create a test for user sign-up"** → Create only the sign-up test, not the full suite
- **"Generate E2E tests for the login page"** → Create comprehensive test suite with all scenarios
- **"Add a test to verify form validation"** → Add only the validation test to existing spec
- **"Create tests for the home page"** → Create full test suite for home functionality
- **"Create a new test e2e for sign-up"** → Create only the specific test mentioned
- **"Generate comprehensive E2E tests for sign-up"** → Create full test suite
**Key Phrases to Identify Scope:**
- **Single Test**: "a test", "one test", "new test", "add test"
- **Full Suite**: "comprehensive tests", "all tests", "test suite", "complete tests", "generate tests"
#### Page Object Model Pattern
- **Extend BasePage**: All page objects should extend `BasePage` for common functionality
- **REUSE Existing Page Objects**: Always check for existing page objects before creating new ones
- **Interface Definitions**: Define clear interfaces for form data and credentials
- **Method Organization**: Group methods by functionality (navigation, form interaction, validation, etc.)
- **Locator Strategy**: Use stable selectors (name attributes, labels) over fragile CSS selectors
- **Avoid Code Duplication**: When creating a new page object, verify if there are repeated methods across page objects that should be moved to `BasePage`
- **Shared Utilities**: If utility functions are repeated across tests, create or update `tests/helpers.ts` to centralize them
- **Refactor to BasePage**: Common patterns like form validation, notification checks, or navigation should be extracted to `BasePage`
- **Refactor to Helpers**: Data generation, test setup utilities, or common assertions should be extracted to `tests/helpers.ts`
#### Page Object Reuse Guidelines
- **Check existing page objects first**: Look in `tests/` directory for existing page objects
- **Import and reuse**: Use existing page objects like `SignInPage`, `HomePage`, etc.
- **Create page objects when needed**: If a test requires interaction with a page that doesn't have a page object yet, create it following the Page Object Model pattern
- **Only create new page objects** when the page doesn't exist or has unique functionality
- **Example**: For a sign-up test that needs to verify login after signup, reuse `SignInPage` and `HomePage` if they exist, or create them if needed
- **Avoid duplication**: Don't recreate functionality that already exists in other page objects
- **Complete dependencies**: When creating a test that requires multiple page interactions, ensure all necessary page objects exist (create them if they don't)
#### Code Refactoring Guidelines
**When to move code to `BasePage`:**
- ✅ **Navigation helpers** used by multiple pages (e.g., `waitForPageLoad()`, `getCurrentUrl()`)
- ✅ **Common UI interactions** (e.g., clicking notifications, handling modals, theme toggles)
- ✅ **Verification patterns** repeated across pages (e.g., `isVisible()`, `waitForVisible()`)
- ✅ **Error handling** that applies to all pages
- ✅ **Screenshot utilities** for debugging
**When to move code to `tests/helpers.ts`:**
- ✅ **Test data generation** (e.g., `generateUniqueEmail()`, `generateTestUser()`)
- ✅ **Setup/teardown utilities** (e.g., `createTestUser()`, `cleanupTestData()`)
- ✅ **Custom assertions** used across tests (e.g., `expectNotificationToContain()`)
- ✅ **API helpers** for test setup (e.g., `seedDatabase()`, `resetState()`)
- ✅ **Time utilities** (e.g., `waitForCondition()`, `retryAction()`)
**Example - Before Refactoring:**
```typescript
// ❌ BAD: Repeated code in multiple page objects
export class SignUpPage extends BasePage {
async waitForNotification(): Promise<void> {
await this.page.waitForSelector('[role="status"]');
}
}
export class SignInPage extends BasePage {
async waitForNotification(): Promise<void> {
await this.page.waitForSelector('[role="status"]');
}
}
```
**Example - After Refactoring:**
```typescript
// ✅ GOOD: Move to BasePage
export class BasePage {
async waitForNotification(): Promise<void> {
await this.page.waitForSelector('[role="status"]');
}
async verifyNotificationMessage(message: string): Promise<void> {
const notification = this.page.locator('[role="status"]');
await expect(notification).toContainText(message);
}
}
// ✅ GOOD: Move to helpers.ts for data generation
export function generateUniqueEmail(): string {
const timestamp = Date.now();
return `test.user.${timestamp}@example.com`;
}
export function generateTestUser() {
return {
name: "Test User",
email: generateUniqueEmail(),
password: "TestPassword123!",
};
}
```
**Page Object Reuse Example:**
```typescript
// ✅ GOOD: Check for existing page objects, create if needed
// 1. Check if SignInPage exists - if not, create it
// 2. Check if HomePage exists - if not, create it
import { SignInPage } from "../sign-in/sign-in-page";
import { HomePage } from "../home/home-page";
test("User can sign up and login", async ({ page }) => {
const signUpPage = new SignUpPage(page);
const signInPage = new SignInPage(page); // REUSE existing (or create if missing)
const homePage = new HomePage(page); // REUSE existing (or create if missing)
// Use existing functionality
await signUpPage.signUp(userData);
await homePage.verifyPageLoaded(); // REUSE existing method
await homePage.signOut(); // REUSE existing method
await signInPage.login(credentials); // REUSE existing method
});
// ❌ BAD: Don't recreate existing functionality in SignUpPage
export class SignUpPage extends BasePage {
// Don't recreate logout functionality
async logout() {
/* ... */
} // ❌ HomePage already has this
// Don't recreate login functionality
async login() {
/* ... */
} // ❌ SignInPage already has this
// ✅ GOOD: Instead, use composition or delegation
async loginAfterSignUp(credentials: LoginCredentials): Promise<void> {
// Reuse SignInPage methods or delegate to it
const emailField = this.page.getByRole("textbox", { name: "Email*" });
const passwordField = this.page.getByRole("textbox", { name: "Password*" });
const loginButton = this.page.getByRole("button", { name: "Log in" });
await emailField.fill(credentials.email);
await passwordField.fill(credentials.password);
await loginButton.click();
}
}
```
**Page Object Structure:**
```typescript
export interface FeatureData {
email: string;
password: string;
// ... other fields
}
export class FeaturePage extends BasePage {
// Form elements
readonly emailInput: Locator;
readonly passwordInput: Locator;
readonly submitButton: Locator;
constructor(page: Page) {
super(page);
// Use stable selectors
this.emailInput = page.getByLabel("Email");
this.passwordInput = page.locator('input[name="password"]');
this.submitButton = page.getByRole("button", { name: "Submit" });
}
async goto(): Promise<void> {
await super.goto("/feature-path");
}
async performAction(data: FeatureData): Promise<void> {
await this.emailInput.fill(data.email);
await this.passwordInput.fill(data.password);
await this.submitButton.click();
}
async verifyCriticalOutcome(): Promise<void> {
await expect(this.page).toHaveURL("/expected-path");
// ... verification logic
}
}
```
#### Test Structure Best Practices
- **Page Object Usage**: Use Page Object Models for all page interactions
- **Tag Organization**: Use Playwright tag syntax for test categorization
- **Test IDs**: Include test case IDs in tags for traceability
- **Verification Steps**: Include clear verification steps for each major action
**Key Elements:**
- **Page Objects**: All interactions through Page Object Models
- **Clear Tags**: Use `{ tag: ['@priority', '@type', '@feature', '@test-id'] }` syntax
- **Verification**: Explicit verification of critical outcomes
#### Playwright Selector Best Practices
When creating locators in Page Objects, follow this priority order for maximum reliability:
**✅ Primary Selectors (Recommended):**
- **`getByRole()`**: The best and most robust for all interactive elements (buttons, links, main sections)
- **`getByLabel()`**: The best for form controls that have an associated label
**⚠️ Secondary Selectors (Use Sparingly):**
- **`getByText()`**: Use only when the above fail or for static text verification (headings, paragraphs, messages)
- **Others (e.g. `getByTestId()`)**: Use only as a last resort when the above fail or are not applicable
**Examples:**
```typescript
// ✅ GOOD - Using getByRole for interactive elements
this.submitButton = page.getByRole("button", { name: "Submit" });
this.navigationLink = page.getByRole("link", { name: "Dashboard" });
// ✅ GOOD - Using getByLabel for form controls
this.emailInput = page.getByLabel("Email");
this.passwordInput = page.getByLabel("Password");
// ⚠️ SPARINGLY - Using getByText only when necessary
this.errorMessage = page.getByText("Invalid credentials"); // Only if no better selector exists
this.pageTitle = page.getByText("Welcome to Prowler"); // Only for static content verification
// ❌ AVOID - Using fragile selectors when better options exist
this.submitButton = page.locator(".btn-primary"); // Use getByRole instead
this.emailInput = page.locator("#email"); // Use getByLabel instead
```
**Tag Syntax Example:**
```typescript
test(
"Test description",
{ tag: ["@critical", "@e2e", "@signup", "@SIGNUP-E2E-001"] },
async ({ page }) => {
// Test implementation
},
);
```
#### E2E Test Documentation Format
Each test documentation file (`{page-name}.md`) should follow this structured format:
```markdown
### E2E Tests: {Feature Name}
**Suite ID:** `{SUITE-ID}`
**Feature:** {Feature description}
---
## Test Case: `{TEST-ID}` - {Test case title}
**Priority:** `{critical|high|medium|low}`
**Tags:**
- type → @e2e
- feature → @{feature-name}
**Description/Objective:** {Brief description of what the test validates}
**Preconditions:**
- {List of prerequisites for the test to run}
- {Any required data or state}
### Flow Steps:
1. {Step 1 description}
2. {Step 2 description}
3. {Step 3 description}
...
### Expected Result:
- {Expected outcome 1}
- {Expected outcome 2}
...
### Key verification points:
- {Key assertion 1}
- {Key assertion 2}
- {Key assertion 3}
### Notes:
- {Any additional notes or considerations}
- {Test data requirements or constraints}
```
#### Test Documentation Best Practices
- **Suite ID Format**: Use descriptive suite IDs (e.g., `SIGNUP-E2E`)
- **Test ID Format**: Include feature and sequence (e.g., `SIGNUP-E2E-001`)
- **Priority Levels**: Use `critical`, `high`, `medium`, `low` for test prioritization
- **Tag Organization**: Use Playwright tag syntax: `{ tag: ['@priority', '@type', '@feature', '@test-id'] }`
- **Flow Steps**: Number steps clearly and describe user actions
- **Verification Points**: List specific assertions and expected outcomes
- **Preconditions**: Document any required setup or data dependencies
- **Test Data Notes**: Include information about data generation and uniqueness strategies
**Tag Categories:**
- **Priority**: `@critical`, `@high`, `@medium`, `@low`
- **Type**: `@e2e`
- **Feature**: `@signup`, `@signin`, `@dashboard`
- **Test ID**: `@SIGNUP-E2E-001`, `@LOGIN-E2E-002`
**IMPORTANT - Keep Documentation Concise:**
- ❌ **DO NOT** include general test running instructions
- ❌ **DO NOT** include file structure explanations
- ❌ **DO NOT** include code examples or tutorials
- ❌ **DO NOT** include extensive troubleshooting sections
- ❌ **DO NOT** include command references or configuration details
- ✅ **DO** focus only on the specific test case: flow, preconditions, expected results, and verification points
- ✅ **DO** keep the documentation under 60 lines when possible
- ✅ **DO** follow the exact format template provided above
### Component Testing (Future)
- Jest + React Testing Library
- Component unit tests
- Integration tests for complex flows
+4 -4
View File
@@ -15,10 +15,10 @@
"format:check": "./node_modules/.bin/prettier --check ./app",
"format:write": "./node_modules/.bin/prettier --config .prettierrc.json --write ./app",
"prepare": "husky",
"test:e2e": "playwright test --project=chromium --project=sign-up",
"test:e2e:ui": "playwright test --project=chromium --project=sign-up --ui",
"test:e2e:debug": "playwright test --project=chromium --project=sign-up --debug",
"test:e2e:headed": "playwright test --project=chromium --project=sign-up --headed",
"test:e2e": "playwright test --project=chromium",
"test:e2e:ui": "playwright test --project=chromium --ui",
"test:e2e:debug": "playwright test --project=chromium --debug",
"test:e2e:headed": "playwright test --project=chromium --headed",
"test:e2e:report": "playwright show-report",
"test:e2e:install": "playwright install"
},
-6
View File
@@ -89,12 +89,6 @@ export default defineConfig({
{
name: "chromium",
use: { ...devices["Desktop Chrome"] },
testMatch: "auth-login.spec.ts",
},
// This project runs the sign-up test suite
{
name: "sign-up",
testMatch: "sign-up.spec.ts",
},
],
@@ -20,7 +20,7 @@ import {
ERROR_MESSAGES,
URLS,
verifyLoadingState,
} from "../helpers";
} from "./helpers";
test.describe("Login Flow", () => {
test.beforeEach(async ({ page }) => {
-159
View File
@@ -1,159 +0,0 @@
import { Page, Locator, expect } from "@playwright/test";
/**
* Base page object class containing common functionality
* that can be shared across all page objects
*/
export abstract class BasePage {
readonly page: Page;
// Common UI elements that appear on most pages
readonly title: Locator;
readonly loadingIndicator: Locator;
readonly themeToggle: Locator;
constructor(page: Page) {
this.page = page;
// Common locators that most pages share
this.title = page.locator("h1, h2, [role='heading']").first();
this.loadingIndicator = page.getByRole("status", { name: "Loading" });
this.themeToggle = page.getByRole("button", { name: "Toggle theme" });
}
// Common navigation methods
async goto(url: string): Promise<void> {
await this.page.goto(url);
await this.waitForPageLoad();
}
async waitForPageLoad(): Promise<void> {
await this.page.waitForLoadState("networkidle");
}
async refresh(): Promise<void> {
await this.page.reload();
await this.waitForPageLoad();
}
async goBack(): Promise<void> {
await this.page.goBack();
await this.waitForPageLoad();
}
// Common verification methods
async verifyPageTitle(expectedTitle: string | RegExp): Promise<void> {
await expect(this.page).toHaveTitle(expectedTitle);
}
async verifyLoadingState(): Promise<void> {
await expect(this.loadingIndicator).toBeVisible();
}
async verifyNoLoadingState(): Promise<void> {
await expect(this.loadingIndicator).not.toBeVisible();
}
// Common form interaction methods
async clearInput(input: Locator): Promise<void> {
await input.clear();
}
async fillInput(input: Locator, value: string): Promise<void> {
await input.fill(value);
}
async clickButton(button: Locator): Promise<void> {
await button.click();
}
// Common validation methods
async verifyElementVisible(element: Locator): Promise<void> {
await expect(element).toBeVisible();
}
async verifyElementNotVisible(element: Locator): Promise<void> {
await expect(element).not.toBeVisible();
}
async verifyElementText(element: Locator, expectedText: string): Promise<void> {
await expect(element).toHaveText(expectedText);
}
async verifyElementContainsText(element: Locator, expectedText: string): Promise<void> {
await expect(element).toContainText(expectedText);
}
// Common accessibility methods
async verifyKeyboardNavigation(elements: Locator[]): Promise<void> {
for (const element of elements) {
await this.page.keyboard.press("Tab");
await expect(element).toBeFocused();
}
}
async verifyAriaLabels(elements: { locator: Locator; expectedLabel: string }[]): Promise<void> {
for (const { locator, expectedLabel } of elements) {
await expect(locator).toHaveAttribute("aria-label", expectedLabel);
}
}
// Common utility methods
async getElementText(element: Locator): Promise<string> {
return await element.textContent() || "";
}
async getElementValue(element: Locator): Promise<string> {
return await element.inputValue();
}
async isElementVisible(element: Locator): Promise<boolean> {
return await element.isVisible();
}
async isElementEnabled(element: Locator): Promise<boolean> {
return await element.isEnabled();
}
// Common error handling methods
async getFormErrors(): Promise<string[]> {
const errorElements = await this.page.locator('[role="alert"], .error-message, [data-testid="error"]').all();
const errors: string[] = [];
for (const element of errorElements) {
const text = await element.textContent();
if (text) {
errors.push(text.trim());
}
}
return errors;
}
async verifyNoErrors(): Promise<void> {
const errors = await this.getFormErrors();
expect(errors).toHaveLength(0);
}
// Common wait methods
async waitForElement(element: Locator, timeout: number = 5000): Promise<void> {
await element.waitFor({ timeout });
}
async waitForElementToDisappear(element: Locator, timeout: number = 5000): Promise<void> {
await element.waitFor({ state: "hidden", timeout });
}
async waitForUrl(expectedUrl: string | RegExp, timeout: number = 5000): Promise<void> {
await this.page.waitForURL(expectedUrl, { timeout });
}
// Common screenshot methods
async takeScreenshot(name: string): Promise<void> {
await this.page.screenshot({ path: `screenshots/${name}.png` });
}
async takeElementScreenshot(element: Locator, name: string): Promise<void> {
await element.screenshot({ path: `screenshots/${name}.png` });
}
}
+1 -13
View File
@@ -1,5 +1,5 @@
import { Page, expect } from "@playwright/test";
import { SignInPage, SignInCredentials } from "./sign-in/sign-in-page";
import { SignInPage, SignInCredentials } from "./page-objects/sign-in-page";
export const ERROR_MESSAGES = {
INVALID_CREDENTIALS: "Invalid email or password",
@@ -162,18 +162,6 @@ export async function authenticateAndSaveState(
await page.context().storageState({ path: storagePath });
}
/**
* Generate a random base36 suffix of specified length
* Used for creating unique test data to avoid conflicts
*/
export function makeSuffix(len: number): string {
let s = "";
while (s.length < len) {
s += Math.random().toString(36).slice(2);
}
return s.slice(0, len);
}
export async function getSession(page: Page) {
const response = await page.request.get("/api/auth/session");
return response.json();
@@ -1,7 +1,7 @@
import { Page, Locator, expect } from "@playwright/test";
import { BasePage } from "../base-page";
export class HomePage extends BasePage {
export class HomePage {
readonly page: Page;
// Main content elements
readonly mainContent: Locator;
@@ -18,14 +18,15 @@ export class HomePage extends BasePage {
readonly overviewSection: Locator;
// UI elements
readonly themeToggle: Locator;
readonly logo: Locator;
constructor(page: Page) {
super(page);
this.page = page;
// Main content elements
this.mainContent = page.locator("main");
this.breadcrumbs = page.getByRole("navigation", { name: "Breadcrumbs" });
this.breadcrumbs = page.getByLabel("Breadcrumbs");
this.overviewHeading = page.getByRole("heading", { name: "Overview", exact: true });
// Navigation elements
@@ -38,12 +39,18 @@ export class HomePage extends BasePage {
this.overviewSection = page.locator('[data-testid="overview-section"]');
// UI elements
this.themeToggle = page.getByLabel("Toggle theme");
this.logo = page.locator('svg[width="300"]');
}
// Navigation methods
async goto(): Promise<void> {
await super.goto("/");
await this.page.goto("/");
await this.waitForPageLoad();
}
async waitForPageLoad(): Promise<void> {
await this.page.waitForLoadState("networkidle");
}
// Verification methods
@@ -51,6 +58,7 @@ export class HomePage extends BasePage {
await expect(this.page).toHaveURL("/");
await expect(this.mainContent).toBeVisible();
await expect(this.overviewHeading).toBeVisible();
await this.waitForPageLoad();
}
async verifyBreadcrumbs(): Promise<void> {
@@ -86,6 +94,15 @@ export class HomePage extends BasePage {
}
// Utility methods
async refresh(): Promise<void> {
await this.page.reload();
await this.waitForPageLoad();
}
async goBack(): Promise<void> {
await this.page.goBack();
await this.waitForPageLoad();
}
// Accessibility methods
async verifyKeyboardNavigation(): Promise<void> {
@@ -94,6 +111,11 @@ export class HomePage extends BasePage {
await expect(this.themeToggle).toBeFocused();
}
// Wait methods
async waitForRedirect(expectedUrl: string): Promise<void> {
await this.page.waitForURL(expectedUrl);
}
async waitForContentLoad(): Promise<void> {
await this.page.waitForFunction(() => {
const main = document.querySelector("main");
@@ -1,6 +1,5 @@
import { Page, Locator, expect } from "@playwright/test";
import { BasePage } from "../base-page";
import { HomePage } from "../home/home-page";
import { HomePage } from "./home-page";
export interface SignInCredentials {
email: string;
@@ -12,7 +11,8 @@ export interface SocialAuthConfig {
githubEnabled: boolean;
}
export class SignInPage extends BasePage {
export class SignInPage {
readonly page: Page;
readonly homePage: HomePage;
// Form elements
@@ -31,48 +31,59 @@ export class SignInPage extends BasePage {
readonly backButton: Locator;
// UI elements
readonly title: Locator;
readonly logo: Locator;
readonly themeToggle: Locator;
// Error messages
readonly errorMessages: Locator;
readonly loadingIndicator: Locator;
// SAML specific elements
readonly samlModeTitle: Locator;
readonly samlEmailInput: Locator;
constructor(page: Page) {
super(page);
this.page = page;
this.homePage = new HomePage(page);
// Form elements
this.emailInput = page.getByRole("textbox", { name: "Email" });
this.passwordInput = page.getByRole("textbox", { name: "Password" });
this.emailInput = page.getByLabel("Email");
this.passwordInput = page.getByLabel("Password");
this.loginButton = page.getByRole("button", { name: "Log in" });
this.form = page.locator("form");
// Social authentication buttons
this.googleButton = page.getByRole("button", { name: "Continue with Google" });
this.githubButton = page.getByRole("button", { name: "Continue with Github" });
this.samlButton = page.getByRole("button", { name: "Continue with SAML SSO" });
this.googleButton = page.getByText("Continue with Google");
this.githubButton = page.getByText("Continue with Github");
this.samlButton = page.getByText("Continue with SAML SSO");
// Navigation elements
this.signUpLink = page.getByRole("link", { name: "Sign up" });
this.backButton = page.getByRole("button", { name: "Back" });
this.backButton = page.getByText("Back");
// UI elements
this.title = page.getByText("Sign in", { exact: true });
this.logo = page.locator('svg[width="300"]');
this.themeToggle = page.getByLabel("Toggle theme");
// Error messages
this.errorMessages = page.locator('[role="alert"], .error-message, [data-testid="error"]');
this.loadingIndicator = page.getByText("Loading");
// SAML specific elements
this.samlModeTitle = page.getByRole("heading", { name: "Sign in with SAML SSO" });
this.samlEmailInput = page.getByRole("textbox", { name: "Email" });
this.samlModeTitle = page.getByText("Sign in with SAML SSO");
this.samlEmailInput = page.getByLabel("Email");
}
// Navigation methods
async goto(): Promise<void> {
await super.goto("/sign-in");
await this.page.goto("/sign-in");
await this.waitForPageLoad();
}
async waitForPageLoad(): Promise<void> {
await this.page.waitForLoadState("networkidle");
}
// Form interaction methods
@@ -137,7 +148,8 @@ export class SignInPage extends BasePage {
async verifyPageLoaded(): Promise<void> {
await expect(this.page).toHaveTitle(/Prowler/);
await expect(this.logo).toBeVisible();
await expect(this.page.getByRole("heading", { name: "Sign in", exact: true })).toBeVisible();
await expect(this.title).toBeVisible();
await this.waitForPageLoad();
}
async verifyFormElements(): Promise<void> {
@@ -157,7 +169,7 @@ export class SignInPage extends BasePage {
}
async verifyNavigationLinks(): Promise<void> {
await expect(this.page.getByRole('link', { name: /Need to create an account\?/i })).toBeVisible();
await expect(this.page.getByText("Need to create an account?")).toBeVisible();
await expect(this.signUpLink).toBeVisible();
}
@@ -166,7 +178,7 @@ export class SignInPage extends BasePage {
}
async verifyLoginError(errorMessage: string = "Invalid email or password"): Promise<void> {
await expect(this.page.getByRole("alert", { name: errorMessage })).toBeVisible();
await expect(this.page.getByText(errorMessage).first()).toBeVisible();
await expect(this.page).toHaveURL("/sign-in");
}
@@ -177,19 +189,19 @@ export class SignInPage extends BasePage {
}
async verifyNormalModeActive(): Promise<void> {
await expect(this.page.getByRole("heading", { name: "Sign in", exact: true })).toBeVisible();
await expect(this.title).toBeVisible();
await expect(this.passwordInput).toBeVisible();
}
async verifyLoadingState(): Promise<void> {
await expect(this.loginButton).toHaveAttribute("aria-disabled", "true");
await super.verifyLoadingState();
await expect(this.loadingIndicator).toBeVisible();
}
async verifyFormValidation(): Promise<void> {
// Check for common validation messages
const emailError = this.page.getByRole("alert", { name: "Please enter a valid email address." });
const passwordError = this.page.getByRole("alert", { name: "Password is required." });
const emailError = this.page.getByText("Please enter a valid email address.");
const passwordError = this.page.getByText("Password is required.");
// At least one validation error should be visible
await expect(emailError.or(passwordError)).toBeVisible();
@@ -228,7 +240,30 @@ export class SignInPage extends BasePage {
return emailValue.length > 0 && passwordValue.length > 0;
}
async getFormErrors(): Promise<string[]> {
const errorElements = await this.errorMessages.all();
const errors: string[] = [];
for (const element of errorElements) {
const text = await element.textContent();
if (text) {
errors.push(text.trim());
}
}
return errors;
}
// Browser interaction methods
async refresh(): Promise<void> {
await this.page.reload();
await this.waitForPageLoad();
}
async goBack(): Promise<void> {
await this.page.goBack();
await this.waitForPageLoad();
}
// Session management methods
async logout(): Promise<void> {
@@ -237,7 +272,7 @@ export class SignInPage extends BasePage {
async verifyLogoutSuccess(): Promise<void> {
await expect(this.page).toHaveURL("/sign-in");
await expect(this.page.getByRole("heading", { name: "Sign in", exact: true })).toBeVisible();
await expect(this.title).toBeVisible();
}
// Advanced interaction methods
@@ -260,7 +295,7 @@ export class SignInPage extends BasePage {
// Error handling methods
async handleSamlError(): Promise<void> {
const samlError = this.page.getByRole("alert", { name: "SAML Authentication Error" });
const samlError = this.page.getByText("SAML Authentication Error");
if (await samlError.isVisible()) {
// Handle SAML error if present
console.log("SAML authentication error detected");
-117
View File
@@ -1,117 +0,0 @@
import { Page, Locator, expect } from "@playwright/test";
import { BasePage } from "../base-page";
export interface SignUpData {
name: string;
email: string;
password: string;
confirmPassword: string;
company?: string;
invitationToken?: string | null;
acceptTerms?: boolean;
}
export class SignUpPage extends BasePage {
// Form inputs
readonly nameInput: Locator;
readonly companyInput: Locator;
readonly emailInput: Locator;
readonly passwordInput: Locator;
readonly confirmPasswordInput: Locator;
readonly invitationTokenInput: Locator;
// UI elements
readonly submitButton: Locator;
readonly loginLink: Locator;
readonly termsCheckbox: Locator;
constructor(page: Page) {
super(page);
// Prefer stable name attributes to avoid label ambiguity in composed inputs
this.nameInput = page.locator('input[name="name"]');
this.companyInput = page.locator('input[name="company"]');
this.emailInput = page.getByRole("textbox", { name: "Email" });
this.passwordInput = page.locator('input[name="password"]');
this.confirmPasswordInput = page.locator('input[name="confirmPassword"]');
this.invitationTokenInput = page.locator('input[name="invitationToken"]');
this.submitButton = page.getByRole("button", { name: "Sign up" });
this.loginLink = page.getByRole("link", { name: "Log in" });
this.termsCheckbox = page.getByRole("checkbox", { name: /I agree with the/i });
}
async goto(): Promise<void> {
await super.goto("/sign-up");
}
async verifyPageLoaded(): Promise<void> {
await expect(this.page.getByRole("heading", { name: "Sign up" })).toBeVisible();
await expect(this.emailInput).toBeVisible();
await expect(this.submitButton).toBeVisible();
await this.waitForPageLoad();
}
async fillName(name: string): Promise<void> {
await this.nameInput.fill(name);
}
async fillCompany(company?: string): Promise<void> {
if (company) {
await this.companyInput.fill(company);
}
}
async fillEmail(email: string): Promise<void> {
await this.emailInput.fill(email);
}
async fillPassword(password: string): Promise<void> {
await this.passwordInput.fill(password);
}
async fillConfirmPassword(confirmPassword: string): Promise<void> {
await this.confirmPasswordInput.fill(confirmPassword);
}
async fillInvitationToken(token?: string | null): Promise<void> {
if (token) {
await this.invitationTokenInput.fill(token);
}
}
async acceptTermsIfPresent(accept: boolean = true): Promise<void> {
// Only in cloud env; check presence before interacting
if (await this.termsCheckbox.isVisible()) {
if (accept) {
await this.termsCheckbox.click();
}
}
}
async submit(): Promise<void> {
await this.submitButton.click();
}
async signup(data: SignUpData): Promise<void> {
await this.fillName(data.name);
await this.fillCompany(data.company);
await this.fillEmail(data.email);
await this.fillPassword(data.password);
await this.fillConfirmPassword(data.confirmPassword);
await this.fillInvitationToken(data.invitationToken ?? undefined);
await this.acceptTermsIfPresent(data.acceptTerms ?? true);
await this.submit();
}
async verifyRedirectToLogin(): Promise<void> {
await expect(this.page).toHaveURL("/sign-in");
}
async verifyRedirectToEmailVerification(): Promise<void> {
await expect(this.page).toHaveURL("/email-verification");
}
}
-43
View File
@@ -1,43 +0,0 @@
### E2E Tests: User Sign-Up
**Suite ID:** `SIGNUP-E2E`
**Feature:** New user registration flow.
---
## Test Case: `SIGNUP-E2E-001` - Successful new user registration and login
**Priority:** `critical`
**Tags:**
- type → @e2e
- feature → @signup
**Description/Objetive:** Registers a new user with valid data, verifies redirect to Login (OSS), and confirms the user can authenticate.
**Preconditions:**
- Application is running, email domain & password is acceptable for sign-up.
- No existing data in Prowler is required; the test can run on a clean state.
- `E2E_NEW_PASSWORD` environment variable must be set with a valid password for the test.
### Flow Steps:
1. Navigate to the Sign up page.
2. Fill the form with valid data (unique email, valid password, terms accepted).
3. Submit the form.
4. Verify redirect to the Login page.
5. Log in with the newly created credentials.
### Expected Result:
- Sign-up succeeds and redirects to Login.
- User can log in successfully using the created credentials and reach the home page.
### Key verification points:
- After submitting sign-up, the URL changes to `/sign-in`.
- The newly created credentials can be used to sign in successfully.
- After login, the user lands on the home (`/`) and main content is visible.
### Notes:
- Test data uses a random base36 suffix to avoid collisions with email.
- The test requires the `E2E_NEW_PASSWORD` environment variable to be set before running.
-49
View File
@@ -1,49 +0,0 @@
import { test } from "@playwright/test";
import { SignUpPage } from "./sign-up-page";
import { SignInPage } from "../sign-in/sign-in-page";
import { makeSuffix } from "../helpers";
test.describe("Sign Up Flow", () => {
test(
"should register a new user successfully",
{ tag: ["@critical", "@e2e", "@signup", "@SIGNUP-E2E-001"] },
async ({ page }) => {
const password = process.env.E2E_NEW_PASSWORD;
if (!password) {
throw new Error("E2E_NEW_PASSWORD environment variable is not set");
}
const signUpPage = new SignUpPage(page);
await signUpPage.goto();
// Generate unique test data
const suffix = makeSuffix(10);
const uniqueEmail = `e2e+${suffix}@prowler.com`;
// Fill and submit the sign-up form
await signUpPage.signup({
name: `E2E User ${suffix}`,
company: `Test E2E Co ${suffix}`,
email: uniqueEmail,
password: password,
confirmPassword: password,
acceptTerms: true,
});
// Verify no errors occurred during sign-up
await signUpPage.verifyNoErrors();
// Verify redirect to login page (OSS environment)
await signUpPage.verifyRedirectToLogin();
// Verify the newly created user can log in successfully
const signInPage = new SignInPage(page);
await signInPage.login({
email: uniqueEmail,
password: password,
});
await signInPage.verifySuccessfulLogin();
},
);
});
+1 -1
View File
@@ -58,7 +58,7 @@ This two-step approach follows the Prowler API design where providers and their
export PROWLER_API_KEY="pk_example-api-key"
```
For detailed instructions on creating API keys, see: https://docs.prowler.com/user-guide/tutorials/prowler-app-api-keys
For detailed instructions on creating API keys, see: https://docs.prowler.com/user-guide/providers/prowler-app-api-keys
## AWS Organizations Integration