Compare commits

..

1 Commits

Author SHA1 Message Date
prowler-bot 4a8607b753 feat(aws): update regions for AWS services 2026-03-02 09:13:19 +00:00
672 changed files with 6455 additions and 30330 deletions
-350
View File
@@ -1,350 +0,0 @@
#!/usr/bin/env bash
#
# Test script for E2E test path resolution logic from ui-e2e-tests-v2.yml.
# Validates that the shell logic correctly transforms E2E_TEST_PATHS into
# Playwright-compatible paths.
#
# Usage: .github/scripts/test-e2e-path-resolution.sh
set -euo pipefail
# -- Colors ------------------------------------------------------------------
RED='\033[0;31m'
GREEN='\033[0;32m'
BOLD='\033[1m'
RESET='\033[0m'
# -- Counters ----------------------------------------------------------------
TOTAL=0
PASSED=0
FAILED=0
# -- Temp directory setup & cleanup ------------------------------------------
TMPDIR_ROOT="$(mktemp -d)"
trap 'rm -rf "$TMPDIR_ROOT"' EXIT
# ---------------------------------------------------------------------------
# create_test_tree DIR [SUBDIRS_WITH_TESTS...]
#
# Creates a fake ui/tests/ tree inside DIR.
# All standard subdirs are created (empty).
# For each name in SUBDIRS_WITH_TESTS, a fake .spec.ts file is placed inside.
# ---------------------------------------------------------------------------
create_test_tree() {
local base="$1"; shift
local all_subdirs=(
auth home invitations profile providers scans
setups sign-in-base sign-up attack-paths findings
compliance browse manage-groups roles users overview
integrations
)
for d in "${all_subdirs[@]}"; do
mkdir -p "${base}/tests/${d}"
done
# Populate requested subdirs with a fake test file
for d in "$@"; do
mkdir -p "${base}/tests/${d}"
touch "${base}/tests/${d}/example.spec.ts"
done
}
# ---------------------------------------------------------------------------
# resolve_paths E2E_TEST_PATHS WORKING_DIR
#
# Extracted EXACT logic from .github/workflows/ui-e2e-tests-v2.yml lines 212-250.
# Outputs space-separated TEST_PATHS, or "SKIP" if no tests found.
# Must be run with WORKING_DIR as the cwd equivalent (we cd into it).
# ---------------------------------------------------------------------------
resolve_paths() {
local E2E_TEST_PATHS="$1"
local WORKING_DIR="$2"
(
cd "$WORKING_DIR"
# --- Line 212-214: strip ui/ prefix, strip **, deduplicate ---------------
TEST_PATHS="${E2E_TEST_PATHS}"
TEST_PATHS=$(echo "$TEST_PATHS" | sed 's|ui/||g' | sed 's|\*\*||g' | tr ' ' '\n' | sort -u)
# --- Line 216: drop setup helpers ----------------------------------------
TEST_PATHS=$(echo "$TEST_PATHS" | grep -v '^tests/setups/' || true)
# --- Lines 219-230: safety net for bare tests/ --------------------------
if echo "$TEST_PATHS" | grep -qx 'tests/'; then
SPECIFIC_DIRS=""
for dir in tests/*/; do
[[ "$dir" == "tests/setups/" ]] && continue
SPECIFIC_DIRS="${SPECIFIC_DIRS}${dir}"$'\n'
done
TEST_PATHS=$(echo "$TEST_PATHS" | grep -vx 'tests/' || true)
TEST_PATHS="${TEST_PATHS}"$'\n'"${SPECIFIC_DIRS}"
TEST_PATHS=$(echo "$TEST_PATHS" | grep -v '^$' | sort -u)
fi
# --- Lines 231-234: bail if empty ----------------------------------------
if [[ -z "$TEST_PATHS" ]]; then
echo "SKIP"
return
fi
# --- Lines 236-245: filter dirs with no test files -----------------------
VALID_PATHS=""
while IFS= read -r p; do
[[ -z "$p" ]] && continue
if find "$p" -name '*.spec.ts' -o -name '*.test.ts' 2>/dev/null | head -1 | grep -q .; then
VALID_PATHS="${VALID_PATHS}${p}"$'\n'
fi
done <<< "$TEST_PATHS"
VALID_PATHS=$(echo "$VALID_PATHS" | grep -v '^$')
# --- Lines 246-249: bail if all empty ------------------------------------
if [[ -z "$VALID_PATHS" ]]; then
echo "SKIP"
return
fi
# --- Line 250: final output (space-separated) ---------------------------
echo "$VALID_PATHS" | tr '\n' ' ' | sed 's/ $//'
)
}
# ---------------------------------------------------------------------------
# run_test NAME INPUT EXPECTED_TYPE [EXPECTED_VALUE]
#
# EXPECTED_TYPE is one of:
# "contains <path>" — output must contain this path
# "equals <value>" — output must exactly equal this value
# "skip" — expect SKIP (no runnable tests)
# "not_contains <p>" — output must NOT contain this path
#
# Multiple expectations can be specified by calling assert_* after run_test.
# For convenience, run_test supports a single assertion inline.
# ---------------------------------------------------------------------------
CURRENT_RESULT=""
CURRENT_TEST_NAME=""
run_test() {
local name="$1"
local input="$2"
local expect_type="$3"
local expect_value="${4:-}"
TOTAL=$((TOTAL + 1))
CURRENT_TEST_NAME="$name"
# Create a fresh temp tree per test
local test_dir="${TMPDIR_ROOT}/test_${TOTAL}"
mkdir -p "$test_dir"
# Default populated dirs: scans, providers, auth, home, profile, sign-up, sign-in-base
create_test_tree "$test_dir" scans providers auth home profile sign-up sign-in-base
CURRENT_RESULT=$(resolve_paths "$input" "$test_dir")
_check "$expect_type" "$expect_value"
}
# Like run_test but lets caller specify which subdirs have test files.
run_test_custom_tree() {
local name="$1"
local input="$2"
local expect_type="$3"
local expect_value="${4:-}"
shift 4
local populated_dirs=("$@")
TOTAL=$((TOTAL + 1))
CURRENT_TEST_NAME="$name"
local test_dir="${TMPDIR_ROOT}/test_${TOTAL}"
mkdir -p "$test_dir"
create_test_tree "$test_dir" "${populated_dirs[@]}"
CURRENT_RESULT=$(resolve_paths "$input" "$test_dir")
_check "$expect_type" "$expect_value"
}
_check() {
local expect_type="$1"
local expect_value="$2"
case "$expect_type" in
skip)
if [[ "$CURRENT_RESULT" == "SKIP" ]]; then
_pass
else
_fail "expected SKIP, got: '$CURRENT_RESULT'"
fi
;;
contains)
if [[ "$CURRENT_RESULT" == *"$expect_value"* ]]; then
_pass
else
_fail "expected to contain '$expect_value', got: '$CURRENT_RESULT'"
fi
;;
not_contains)
if [[ "$CURRENT_RESULT" != *"$expect_value"* ]]; then
_pass
else
_fail "expected NOT to contain '$expect_value', got: '$CURRENT_RESULT'"
fi
;;
equals)
if [[ "$CURRENT_RESULT" == "$expect_value" ]]; then
_pass
else
_fail "expected exactly '$expect_value', got: '$CURRENT_RESULT'"
fi
;;
*)
_fail "unknown expect_type: $expect_type"
;;
esac
}
_pass() {
PASSED=$((PASSED + 1))
printf '%b PASS%b %s\n' "$GREEN" "$RESET" "$CURRENT_TEST_NAME"
}
_fail() {
FAILED=$((FAILED + 1))
printf '%b FAIL%b %s\n' "$RED" "$RESET" "$CURRENT_TEST_NAME"
printf " %s\n" "$1"
}
# ===========================================================================
# TEST CASES
# ===========================================================================
echo ""
printf '%bE2E Path Resolution Tests%b\n' "$BOLD" "$RESET"
echo "=========================================="
# 1. Normal single module
run_test \
"1. Normal single module" \
"ui/tests/scans/**" \
"contains" "tests/scans/"
# 2. Multiple modules
run_test \
"2. Multiple modules — scans present" \
"ui/tests/scans/** ui/tests/providers/**" \
"contains" "tests/scans/"
run_test \
"2. Multiple modules — providers present" \
"ui/tests/scans/** ui/tests/providers/**" \
"contains" "tests/providers/"
# 3. Broad pattern (many modules)
run_test \
"3. Broad pattern — no bare tests/" \
"ui/tests/auth/** ui/tests/scans/** ui/tests/providers/** ui/tests/home/** ui/tests/profile/**" \
"not_contains" "tests/ "
# 4. Empty directory
run_test \
"4. Empty directory — skipped" \
"ui/tests/attack-paths/**" \
"skip"
# 5. Mix of populated and empty dirs
run_test \
"5. Mix populated+empty — scans present" \
"ui/tests/scans/** ui/tests/attack-paths/**" \
"contains" "tests/scans/"
run_test \
"5. Mix populated+empty — attack-paths absent" \
"ui/tests/scans/** ui/tests/attack-paths/**" \
"not_contains" "tests/attack-paths/"
# 6. All empty directories
run_test \
"6. All empty directories" \
"ui/tests/attack-paths/** ui/tests/findings/**" \
"skip"
# 7. Setup paths filtered
run_test \
"7. Setup paths filtered out" \
"ui/tests/setups/**" \
"skip"
# 8. Bare tests/ from broad pattern — safety net expands
run_test \
"8. Bare tests/ expands — scans present" \
"ui/tests/**" \
"contains" "tests/scans/"
run_test \
"8. Bare tests/ expands — setups excluded" \
"ui/tests/**" \
"not_contains" "tests/setups/"
# 9. Bare tests/ with all empty subdirs (only setups has files)
run_test_custom_tree \
"9. Bare tests/ — only setups has files" \
"ui/tests/**" \
"skip" "" \
setups
# 10. Duplicate paths
run_test \
"10. Duplicate paths — deduplicated" \
"ui/tests/scans/** ui/tests/scans/**" \
"equals" "tests/scans/"
# 11. Empty input
TOTAL=$((TOTAL + 1))
CURRENT_TEST_NAME="11. Empty input"
test_dir="${TMPDIR_ROOT}/test_${TOTAL}"
mkdir -p "$test_dir"
create_test_tree "$test_dir" scans providers
CURRENT_RESULT=$(resolve_paths "" "$test_dir")
_check "skip" ""
# 12. Trailing/leading whitespace
run_test \
"12. Whitespace handling" \
" ui/tests/scans/** " \
"contains" "tests/scans/"
# 13. Path without ui/ prefix
run_test \
"13. Path without ui/ prefix" \
"tests/scans/**" \
"contains" "tests/scans/"
# 14. Setup mixed with valid paths — only valid pass through
run_test \
"14. Setups + valid — setups filtered" \
"ui/tests/setups/** ui/tests/scans/**" \
"contains" "tests/scans/"
run_test \
"14. Setups + valid — setups absent" \
"ui/tests/setups/** ui/tests/scans/**" \
"not_contains" "tests/setups/"
# ===========================================================================
# SUMMARY
# ===========================================================================
echo ""
echo "=========================================="
if [[ "$FAILED" -eq 0 ]]; then
printf '%b%bAll tests passed: %d/%d%b\n' "$GREEN" "$BOLD" "$PASSED" "$TOTAL" "$RESET"
else
printf '%b%b%d/%d passed, %d FAILED%b\n' "$RED" "$BOLD" "$PASSED" "$TOTAL" "$FAILED" "$RESET"
fi
echo ""
exit "$FAILED"
+6 -54
View File
@@ -224,24 +224,8 @@ modules:
tests:
- api/src/backend/api/tests/test_views.py
e2e:
# All E2E test suites (explicit to avoid triggering auth setups in tests/setups/)
- ui/tests/auth/**
- ui/tests/sign-in/**
- ui/tests/sign-up/**
- ui/tests/sign-in-base/**
- ui/tests/scans/**
- ui/tests/providers/**
- ui/tests/findings/**
- ui/tests/compliance/**
- ui/tests/invitations/**
- ui/tests/roles/**
- ui/tests/users/**
- ui/tests/integrations/**
- ui/tests/resources/**
- ui/tests/profile/**
- ui/tests/lighthouse/**
- ui/tests/home/**
- ui/tests/attack-paths/**
# API view changes can break UI
- ui/tests/**
- name: api-serializers
match:
@@ -250,24 +234,8 @@ modules:
tests:
- api/src/backend/api/tests/**
e2e:
# All E2E test suites (explicit to avoid triggering auth setups in tests/setups/)
- ui/tests/auth/**
- ui/tests/sign-in/**
- ui/tests/sign-up/**
- ui/tests/sign-in-base/**
- ui/tests/scans/**
- ui/tests/providers/**
- ui/tests/findings/**
- ui/tests/compliance/**
- ui/tests/invitations/**
- ui/tests/roles/**
- ui/tests/users/**
- ui/tests/integrations/**
- ui/tests/resources/**
- ui/tests/profile/**
- ui/tests/lighthouse/**
- ui/tests/home/**
- ui/tests/attack-paths/**
# Serializer changes affect API responses → UI
- ui/tests/**
- name: api-filters
match:
@@ -439,24 +407,8 @@ modules:
- ui/components/ui/**
tests: []
e2e:
# All E2E test suites (explicit to avoid triggering auth setups in tests/setups/)
- ui/tests/auth/**
- ui/tests/sign-in/**
- ui/tests/sign-up/**
- ui/tests/sign-in-base/**
- ui/tests/scans/**
- ui/tests/providers/**
- ui/tests/findings/**
- ui/tests/compliance/**
- ui/tests/invitations/**
- ui/tests/roles/**
- ui/tests/users/**
- ui/tests/integrations/**
- ui/tests/resources/**
- ui/tests/profile/**
- ui/tests/lighthouse/**
- ui/tests/home/**
- ui/tests/attack-paths/**
# Shared components can affect any E2E
- ui/tests/**
- name: ui-attack-paths
match:
-48
View File
@@ -1,48 +0,0 @@
name: 'Helm: Chart Checks'
# DISCLAIMER: This workflow is not maintained by the Prowler team. Refer to contrib/k8s/helm/prowler-app for the source code.
on:
push:
branches:
- 'master'
- 'v5.*'
paths:
- 'contrib/k8s/helm/prowler-app/**'
pull_request:
branches:
- 'master'
- 'v5.*'
paths:
- 'contrib/k8s/helm/prowler-app/**'
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
env:
CHART_PATH: contrib/k8s/helm/prowler-app
jobs:
helm-lint:
if: github.repository == 'prowler-cloud/prowler'
runs-on: ubuntu-latest
timeout-minutes: 10
permissions:
contents: read
steps:
- name: Checkout repository
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with:
persist-credentials: false
- name: Set up Helm
uses: azure/setup-helm@1a275c3b69536ee54be43f2070a358922e12c8d4 # v4.3.1
- name: Update chart dependencies
run: helm dependency update ${{ env.CHART_PATH }}
- name: Lint Helm chart
run: helm lint ${{ env.CHART_PATH }}
- name: Validate Helm chart template rendering
run: helm template prowler ${{ env.CHART_PATH }}
-54
View File
@@ -1,54 +0,0 @@
name: 'Helm: Chart Release'
# DISCLAIMER: This workflow is not maintained by the Prowler team. Refer to contrib/k8s/helm/prowler-app for the source code.
on:
release:
types:
- 'published'
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: false
env:
CHART_PATH: contrib/k8s/helm/prowler-app
jobs:
release-helm-chart:
if: github.repository == 'prowler-cloud/prowler'
runs-on: ubuntu-latest
timeout-minutes: 10
permissions:
contents: read
packages: write
steps:
- name: Checkout repository
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with:
persist-credentials: false
- name: Set up Helm
uses: azure/setup-helm@b9e51907a09c216f16ebe8536097933489208112 # v4.3.0
- name: Set appVersion from release tag
run: |
RELEASE_TAG="${GITHUB_EVENT_RELEASE_TAG_NAME}"
echo "Setting appVersion to ${RELEASE_TAG}"
sed -i "s/^appVersion:.*/appVersion: \"${RELEASE_TAG}\"/" ${{ env.CHART_PATH }}/Chart.yaml
env:
GITHUB_EVENT_RELEASE_TAG_NAME: ${{ github.event.release.tag_name }}
- name: Login to GHCR
run: echo "${{ secrets.GITHUB_TOKEN }}" | helm registry login ghcr.io -u ${GITHUB_ACTOR} --password-stdin
- name: Update chart dependencies
run: helm dependency update ${{ env.CHART_PATH }}
- name: Package Helm chart
run: helm package ${{ env.CHART_PATH }} --destination .helm-packages
- name: Push chart to GHCR
run: |
PACKAGE=$(ls .helm-packages/*.tgz)
helm push "$PACKAGE" oci://ghcr.io/${{ github.repository_owner }}/charts
+1 -1
View File
@@ -122,7 +122,7 @@ jobs:
"wafv2": ["cognito", "elbv2"],
}
changed_raw = os.environ.get("STEPS_CHANGED_AWS_OUTPUTS_ALL_CHANGED_FILES", "")
changed_raw = """${STEPS_CHANGED_AWS_OUTPUTS_ALL_CHANGED_FILES}"""
# all_changed_files is space-separated, not newline-separated
# Strip leading "./" if present for consistent path handling
changed_files = [Path(f.lstrip("./")) for f in changed_raw.split() if f]
+1 -30
View File
@@ -214,40 +214,11 @@ jobs:
TEST_PATHS=$(echo "$TEST_PATHS" | sed 's|ui/||g' | sed 's|\*\*||g' | tr ' ' '\n' | sort -u)
# Drop auth setup helpers (not runnable test suites)
TEST_PATHS=$(echo "$TEST_PATHS" | grep -v '^tests/setups/')
# Safety net: if bare "tests/" appears (from broad patterns like ui/tests/**),
# expand to specific subdirs to avoid Playwright discovering setup files
if echo "$TEST_PATHS" | grep -qx 'tests/'; then
echo "Expanding bare 'tests/' to specific subdirs (excluding setups)..."
SPECIFIC_DIRS=""
for dir in tests/*/; do
[[ "$dir" == "tests/setups/" ]] && continue
SPECIFIC_DIRS="${SPECIFIC_DIRS}${dir}"$'\n'
done
# Replace "tests/" with specific dirs, keep other paths
TEST_PATHS=$(echo "$TEST_PATHS" | grep -vx 'tests/')
TEST_PATHS="${TEST_PATHS}"$'\n'"${SPECIFIC_DIRS}"
TEST_PATHS=$(echo "$TEST_PATHS" | grep -v '^$' | sort -u)
fi
if [[ -z "$TEST_PATHS" ]]; then
echo "No runnable E2E test paths after filtering setups"
exit 0
fi
# Filter out directories that don't contain any test files
VALID_PATHS=""
while IFS= read -r p; do
[[ -z "$p" ]] && continue
if find "$p" -name '*.spec.ts' -o -name '*.test.ts' 2>/dev/null | head -1 | grep -q .; then
VALID_PATHS="${VALID_PATHS}${p}"$'\n'
else
echo "Skipping empty test directory: $p"
fi
done <<< "$TEST_PATHS"
VALID_PATHS=$(echo "$VALID_PATHS" | grep -v '^$' || true)
if [[ -z "$VALID_PATHS" ]]; then
echo "No test files found in any resolved paths — skipping E2E"
exit 0
fi
TEST_PATHS=$(echo "$VALID_PATHS" | tr '\n' ' ')
TEST_PATHS=$(echo "$TEST_PATHS" | tr '\n' ' ')
echo "Resolved test paths: $TEST_PATHS"
pnpm exec playwright test $TEST_PATHS
fi
+5 -7
View File
@@ -109,16 +109,14 @@ Every AWS provider scan will enqueue an Attack Paths ingestion job automatically
| GCP | 100 | 13 | 15 | 11 | Official | UI, API, CLI |
| Kubernetes | 83 | 7 | 7 | 9 | Official | UI, API, CLI |
| GitHub | 21 | 2 | 1 | 2 | Official | UI, API, CLI |
| M365 | 89 | 9 | 4 | 5 | Official | UI, API, CLI |
| OCI | 48 | 13 | 3 | 10 | Official | UI, API, CLI |
| M365 | 75 | 7 | 4 | 4 | Official | UI, API, CLI |
| OCI | 51 | 13 | 3 | 12 | Official | UI, API, CLI |
| Alibaba Cloud | 61 | 9 | 3 | 9 | Official | UI, API, CLI |
| Cloudflare | 29 | 2 | 0 | 5 | Official | UI, API, CLI |
| Cloudflare | 29 | 2 | 0 | 5 | Official | CLI, API |
| IaC | [See `trivy` docs.](https://trivy.dev/latest/docs/coverage/iac/) | N/A | N/A | N/A | Official | UI, API, CLI |
| MongoDB Atlas | 10 | 3 | 0 | 8 | Official | UI, API, CLI |
| MongoDB Atlas | 10 | 3 | 0 | 3 | Official | UI, API, CLI |
| LLM | [See `promptfoo` docs.](https://www.promptfoo.dev/docs/red-team/plugins/) | N/A | N/A | N/A | Official | CLI |
| Image | N/A | N/A | N/A | N/A | Official | CLI, API |
| Google Workspace | 1 | 1 | 0 | 1 | Official | CLI |
| OpenStack | 27 | 4 | 0 | 8 | Official | UI, API, CLI |
| OpenStack | 1 | 1 | 0 | 2 | Official | CLI |
| NHN | 6 | 2 | 1 | 0 | Unofficial | CLI |
> [!Note]
+8 -19
View File
@@ -2,22 +2,7 @@
All notable changes to the **Prowler API** are documented in this file.
## [1.21.0] (Prowler v5.20.0)
### 🔄 Changed
- Attack Paths: Migrate network exposure queries from APOC to standard openCypher for Neo4j and Neptune compatibility [(#10266)](https://github.com/prowler-cloud/prowler/pull/10266)
- `POST /api/v1/providers` returns `409 Conflict` if already exists [(#10293)](https://github.com/prowler-cloud/prowler/pull/10293)
### 🐞 Fixed
- Attack Paths: Security hardening for custom query endpoint (Cypher blocklist, input validation, rate limiting, Helm lockdown) [(#10238)](https://github.com/prowler-cloud/prowler/pull/10238)
- Attack Paths: Missing logging for query execution and exception details in scan error handling [(#10269)](https://github.com/prowler-cloud/prowler/pull/10269)
- Attack Paths: Upgrade Cartography from 0.129.0 to 0.132.0, fixing `exposed_internet` not set on ELB/ELBv2 nodes [(#10272)](https://github.com/prowler-cloud/prowler/pull/10272)
---
## [1.20.0] (Prowler v5.19.0)
## [1.20.0] (Prowler UNRELEASED)
### 🚀 Added
@@ -26,16 +11,21 @@ All notable changes to the **Prowler API** are documented in this file.
- PDF report for the CSA CCM compliance framework [(#10088)](https://github.com/prowler-cloud/prowler/pull/10088)
- `image` provider support for container image scanning [(#10128)](https://github.com/prowler-cloud/prowler/pull/10128)
- Attack Paths: Custom query and Cartography schema endpoints (temporarily blocked) [(#10149)](https://github.com/prowler-cloud/prowler/pull/10149)
- `googleworkspace` provider support [(#10247)](https://github.com/prowler-cloud/prowler/pull/10247)
### 🔄 Changed
- Attack Paths: Queries definition now has short description and attribution [(#9983)](https://github.com/prowler-cloud/prowler/pull/9983)
- Attack Paths: Internet node is created while scan [(#9992)](https://github.com/prowler-cloud/prowler/pull/9992)
- Attack Paths: Add full paths set from [pathfinding.cloud](https://pathfinding.cloud/) [(#10008)](https://github.com/prowler-cloud/prowler/pull/10008)
- Support CSA CCM 4.0 for the AWS provider [(#10018)](https://github.com/prowler-cloud/prowler/pull/10018)
- Support CSA CCM 4.0 for the GCP provider [(#10042)](https://github.com/prowler-cloud/prowler/pull/10042)
- Support CSA CCM 4.0 for the Azure provider [(#10039)](https://github.com/prowler-cloud/prowler/pull/10039)
- Support CSA CCM 4.0 for the Oracle Cloud provider [(#10057)](https://github.com/prowler-cloud/prowler/pull/10057)
- Support CSA CCM 4.0 for the Alibaba Cloud provider [(#10061)](https://github.com/prowler-cloud/prowler/pull/10061)
- Attack Paths: Mark attack Paths scan as failed when Celery task fails outside job error handling [(#10065)](https://github.com/prowler-cloud/prowler/pull/10065)
- Attack Paths: Remove legacy per-scan `graph_database` and `is_graph_database_deleted` fields from AttackPathsScan model [(#10077)](https://github.com/prowler-cloud/prowler/pull/10077)
- Attack Paths: Add `graph_data_ready` field to decouple query availability from scan state [(#10089)](https://github.com/prowler-cloud/prowler/pull/10089)
- AI agent guidelines with TDD and testing skills references [(#9925)](https://github.com/prowler-cloud/prowler/pull/9925)
- Attack Paths: Upgrade Cartography from fork 0.126.1 to upstream 0.129.0 and Neo4j driver from 5.x to 6.x [(#10110)](https://github.com/prowler-cloud/prowler/pull/10110)
- Attack Paths: Query results now filtered by provider, preventing future cross-tenant and cross-provider data leakage [(#10118)](https://github.com/prowler-cloud/prowler/pull/10118)
- Attack Paths: Add private labels and properties in Attack Paths graphs for avoiding future overlapping with Cartography's ones [(#10124)](https://github.com/prowler-cloud/prowler/pull/10124)
@@ -45,7 +35,6 @@ All notable changes to the **Prowler API** are documented in this file.
### 🐞 Fixed
- PDF compliance reports consistency with UI: exclude resourceless findings and fix ENS MANUAL status handling [(#10270)](https://github.com/prowler-cloud/prowler/pull/10270)
- Attack Paths: Orphaned temporary Neo4j databases are now cleaned up on scan failure and provider deletion [(#10101)](https://github.com/prowler-cloud/prowler/pull/10101)
- Attack Paths: scan no longer raises `DatabaseError` when provider is deleted mid-scan [(#10116)](https://github.com/prowler-cloud/prowler/pull/10116)
- Tenant compliance summaries recalculated after provider deletion [(#10172)](https://github.com/prowler-cloud/prowler/pull/10172)
@@ -58,7 +47,7 @@ All notable changes to the **Prowler API** are documented in this file.
---
## [1.19.3] (Prowler v5.18.3)
## [1.19.3] (Prowler UNRELEASED)
### 🐞 Fixed
+7
View File
@@ -24,6 +24,13 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
python3-dev \
&& rm -rf /var/lib/apt/lists/*
# Cartography depends on `dockerfile` which has no pre-built arm64 wheel and requires Go to compile
# hadolint ignore=DL3008
RUN if [ "$(uname -m)" = "aarch64" ]; then \
apt-get update && apt-get install -y --no-install-recommends golang-go \
&& rm -rf /var/lib/apt/lists/* ; \
fi
# Install PowerShell
RUN ARCH=$(uname -m) && \
if [ "$ARCH" = "x86_64" ]; then \
+33 -388
View File
File diff suppressed because it is too large Load Diff
+3 -3
View File
@@ -24,7 +24,7 @@ dependencies = [
"drf-spectacular-jsonapi==0.5.1",
"gunicorn==23.0.0",
"lxml==5.3.2",
"prowler @ git+https://github.com/prowler-cloud/prowler.git@v5.20",
"prowler @ git+https://github.com/prowler-cloud/prowler.git@master",
"psycopg2-binary==2.9.9",
"pytest-celery[redis] (>=1.0.1,<2.0.0)",
"sentry-sdk[django] (>=2.20.0,<3.0.0)",
@@ -37,7 +37,7 @@ dependencies = [
"matplotlib (>=3.10.6,<4.0.0)",
"reportlab (>=4.4.4,<5.0.0)",
"neo4j (>=6.0.0,<7.0.0)",
"cartography (==0.132.0)",
"cartography (==0.129.0)",
"gevent (>=25.9.1,<26.0.0)",
"werkzeug (>=3.1.4)",
"sqlparse (>=0.5.4)",
@@ -49,7 +49,7 @@ name = "prowler-api"
package-mode = false
# Needed for the SDK compatibility
requires-python = ">=3.11,<3.13"
version = "1.21.1"
version = "1.20.0"
[project.scripts]
celery = "src.backend.config.settings.celery"
@@ -35,7 +35,6 @@ READ_EXCEPTION_CODES = [
"Neo.ClientError.Statement.AccessMode",
"Neo.ClientError.Procedure.ProcedureNotFound",
]
CLIENT_STATEMENT_EXCEPTION_PREFIX = "Neo.ClientError.Statement."
# Module-level process-wide driver singleton
_driver: neo4j.Driver | None = None
@@ -109,7 +108,6 @@ def get_session(
except neo4j.exceptions.Neo4jError as exc:
if (
default_access_mode == neo4j.READ_ACCESS
and exc.code
and exc.code in READ_EXCEPTION_CODES
):
message = "Read query not allowed"
@@ -117,10 +115,6 @@ def get_session(
raise WriteQueryNotAllowedException(message=message, code=code)
message = exc.message if exc.message is not None else str(exc)
if exc.code and exc.code.startswith(CLIENT_STATEMENT_EXCEPTION_PREFIX):
raise ClientStatementException(message=message, code=exc.code)
raise GraphDatabaseQueryException(message=message, code=exc.code)
finally:
@@ -233,7 +227,3 @@ class GraphDatabaseQueryException(Exception):
class WriteQueryNotAllowedException(GraphDatabaseQueryException):
pass
class ClientStatementException(GraphDatabaseQueryException):
pass
+43 -17
View File
@@ -16,7 +16,8 @@ AWS_INTERNET_EXPOSED_EC2_SENSITIVE_S3_ACCESS = AttackPathsQueryDefinition(
description="Detect EC2 instances with SSH exposed to the internet that can assume higher-privileged roles to read tagged sensitive S3 buckets despite bucket-level public access blocks.",
provider="aws",
cypher=f"""
OPTIONAL MATCH (internet:Internet {{_provider_id: $provider_id}})
CALL apoc.create.vNode(['Internet'], {{id: 'Internet', name: 'Internet', provider_id: $provider_id}})
YIELD node AS internet
MATCH path_s3 = (aws:AWSAccount {{id: $provider_uid}})--(s3:S3Bucket)--(t:AWSTag)
WHERE toLower(t.key) = toLower($tag_key) AND toLower(t.value) = toLower($tag_value)
@@ -31,7 +32,8 @@ AWS_INTERNET_EXPOSED_EC2_SENSITIVE_S3_ACCESS = AttackPathsQueryDefinition(
MATCH path_assume_role = (ec2)-[p:STS_ASSUMEROLE_ALLOW*1..9]-(r:AWSRole)
OPTIONAL MATCH (internet)-[can_access:CAN_ACCESS]->(ec2)
CALL apoc.create.vRelationship(internet, 'CAN_ACCESS', {{provider_id: $provider_id}}, ec2)
YIELD rel AS can_access
UNWIND nodes(path_s3) + nodes(path_ec2) + nodes(path_role) + nodes(path_assume_role) as n
OPTIONAL MATCH (n)-[pfr]-(pf:{PROWLER_FINDING_LABEL} {{status: 'FAIL', provider_uid: $provider_uid}})
@@ -179,12 +181,14 @@ AWS_EC2_INSTANCES_INTERNET_EXPOSED = AttackPathsQueryDefinition(
description="Find EC2 instances flagged as exposed to the internet within the selected account.",
provider="aws",
cypher=f"""
OPTIONAL MATCH (internet:Internet {{_provider_id: $provider_id}})
CALL apoc.create.vNode(['Internet'], {{id: 'Internet', name: 'Internet', provider_id: $provider_id}})
YIELD node AS internet
MATCH path = (aws:AWSAccount {{id: $provider_uid}})--(ec2:EC2Instance)
WHERE ec2.exposed_internet = true
OPTIONAL MATCH (internet)-[can_access:CAN_ACCESS]->(ec2)
CALL apoc.create.vRelationship(internet, 'CAN_ACCESS', {{provider_id: $provider_id}}, ec2)
YIELD rel AS can_access
UNWIND nodes(path) as n
OPTIONAL MATCH (n)-[pfr]-(pf:{PROWLER_FINDING_LABEL} {{status: 'FAIL', provider_uid: $provider_uid}})
@@ -201,14 +205,16 @@ AWS_SECURITY_GROUPS_OPEN_INTERNET_FACING = AttackPathsQueryDefinition(
description="Find internet-facing resources associated with security groups that allow inbound access from '0.0.0.0/0'.",
provider="aws",
cypher=f"""
OPTIONAL MATCH (internet:Internet {{_provider_id: $provider_id}})
CALL apoc.create.vNode(['Internet'], {{id: 'Internet', name: 'Internet', provider_id: $provider_id}})
YIELD node AS internet
// Match EC2 instances that are internet-exposed with open security groups (0.0.0.0/0)
MATCH path_ec2 = (aws:AWSAccount {{id: $provider_uid}})--(ec2:EC2Instance)--(sg:EC2SecurityGroup)--(ipi:IpPermissionInbound)--(ir:IpRange)
WHERE ec2.exposed_internet = true
AND ir.range = "0.0.0.0/0"
OPTIONAL MATCH (internet)-[can_access:CAN_ACCESS]->(ec2)
CALL apoc.create.vRelationship(internet, 'CAN_ACCESS', {{provider_id: $provider_id}}, ec2)
YIELD rel AS can_access
UNWIND nodes(path_ec2) as n
OPTIONAL MATCH (n)-[pfr]-(pf:{PROWLER_FINDING_LABEL} {{status: 'FAIL', provider_uid: $provider_uid}})
@@ -225,12 +231,14 @@ AWS_CLASSIC_ELB_INTERNET_EXPOSED = AttackPathsQueryDefinition(
description="Find Classic Load Balancers exposed to the internet along with their listeners.",
provider="aws",
cypher=f"""
OPTIONAL MATCH (internet:Internet {{_provider_id: $provider_id}})
CALL apoc.create.vNode(['Internet'], {{id: 'Internet', name: 'Internet', provider_id: $provider_id}})
YIELD node AS internet
MATCH path = (aws:AWSAccount {{id: $provider_uid}})--(elb:LoadBalancer)--(listener:ELBListener)
WHERE elb.exposed_internet = true
OPTIONAL MATCH (internet)-[can_access:CAN_ACCESS]->(elb)
CALL apoc.create.vRelationship(internet, 'CAN_ACCESS', {{provider_id: $provider_id}}, elb)
YIELD rel AS can_access
UNWIND nodes(path) as n
OPTIONAL MATCH (n)-[pfr]-(pf:{PROWLER_FINDING_LABEL} {{status: 'FAIL', provider_uid: $provider_uid}})
@@ -247,12 +255,14 @@ AWS_ELBV2_INTERNET_EXPOSED = AttackPathsQueryDefinition(
description="Find ELBv2 load balancers exposed to the internet along with their listeners.",
provider="aws",
cypher=f"""
OPTIONAL MATCH (internet:Internet {{_provider_id: $provider_id}})
CALL apoc.create.vNode(['Internet'], {{id: 'Internet', name: 'Internet', provider_id: $provider_id}})
YIELD node AS internet
MATCH path = (aws:AWSAccount {{id: $provider_uid}})--(elbv2:LoadBalancerV2)--(listener:ELBV2Listener)
WHERE elbv2.exposed_internet = true
OPTIONAL MATCH (internet)-[can_access:CAN_ACCESS]->(elbv2)
CALL apoc.create.vRelationship(internet, 'CAN_ACCESS', {{provider_id: $provider_id}}, elbv2)
YIELD rel AS can_access
UNWIND nodes(path) as n
OPTIONAL MATCH (n)-[pfr]-(pf:{PROWLER_FINDING_LABEL} {{status: 'FAIL', provider_uid: $provider_uid}})
@@ -269,15 +279,31 @@ AWS_PUBLIC_IP_RESOURCE_LOOKUP = AttackPathsQueryDefinition(
description="Given a public IP address, find the related AWS resource and its adjacent node within the selected account.",
provider="aws",
cypher=f"""
OPTIONAL MATCH (internet:Internet {{_provider_id: $provider_id}})
CALL apoc.create.vNode(['Internet'], {{id: 'Internet', name: 'Internet', provider_id: $provider_id}})
YIELD node AS internet
MATCH path = (aws:AWSAccount {{id: $provider_uid}})-[r]-(x)-[q]-(y)
WHERE (x:EC2PrivateIp AND x.public_ip = $ip)
OR (x:EC2Instance AND x.publicipaddress = $ip)
OR (x:NetworkInterface AND x.public_ip = $ip)
OR (x:ElasticIPAddress AND x.public_ip = $ip)
CALL () {{
MATCH path = (aws:AWSAccount {{id: $provider_uid}})-[r]-(x:EC2PrivateIp)-[q]-(y)
WHERE x.public_ip = $ip
RETURN path, x
OPTIONAL MATCH (internet)-[can_access:CAN_ACCESS]->(x)
UNION MATCH path = (aws:AWSAccount {{id: $provider_uid}})-[r]-(x:EC2Instance)-[q]-(y)
WHERE x.publicipaddress = $ip
RETURN path, x
UNION MATCH path = (aws:AWSAccount {{id: $provider_uid}})-[r]-(x:NetworkInterface)-[q]-(y)
WHERE x.public_ip = $ip
RETURN path, x
UNION MATCH path = (aws:AWSAccount {{id: $provider_uid}})-[r]-(x:ElasticIPAddress)-[q]-(y)
WHERE x.public_ip = $ip
RETURN path, x
}}
WITH path, x, internet
CALL apoc.create.vRelationship(internet, 'CAN_ACCESS', {{provider_id: $provider_id}}, x)
YIELD rel AS can_access
UNWIND nodes(path) as n
OPTIONAL MATCH (n)-[pfr]-(pf:{PROWLER_FINDING_LABEL} {{status: 'FAIL', provider_uid: $provider_uid}})
@@ -1,5 +1,4 @@
import logging
import re
from typing import Any, Iterable
@@ -118,38 +117,6 @@ def execute_query(
# Custom query helpers
# Patterns that indicate SSRF or dangerous procedure calls
# Defense-in-depth layer - the primary control is `neo4j.READ_ACCESS`
_BLOCKED_PATTERNS = [
re.compile(r"\bLOAD\s+CSV\b", re.IGNORECASE),
re.compile(r"\bapoc\.load\b", re.IGNORECASE),
re.compile(r"\bapoc\.import\b", re.IGNORECASE),
re.compile(r"\bapoc\.export\b", re.IGNORECASE),
re.compile(r"\bapoc\.cypher\b", re.IGNORECASE),
re.compile(r"\bapoc\.systemdb\b", re.IGNORECASE),
re.compile(r"\bapoc\.config\b", re.IGNORECASE),
re.compile(r"\bapoc\.periodic\b", re.IGNORECASE),
re.compile(r"\bapoc\.do\b", re.IGNORECASE),
re.compile(r"\bapoc\.trigger\b", re.IGNORECASE),
re.compile(r"\bapoc\.custom\b", re.IGNORECASE),
]
# Strip string literals so patterns inside quotes don't cause false positives
# Handles escaped quotes (\' and \") inside strings
_STRING_LITERALS = re.compile(r"'(?:[^'\\]|\\.)*'|\"(?:[^\"\\]|\\.)*\"")
def validate_custom_query(cypher: str) -> None:
"""Reject queries containing known SSRF or dangerous procedure patterns.
Raises ValidationError if a blocked pattern is found.
String literals are stripped before matching to avoid false positives.
"""
stripped = _STRING_LITERALS.sub("", cypher)
for pattern in _BLOCKED_PATTERNS:
if pattern.search(stripped):
raise ValidationError({"query": "Query contains a blocked operation"})
def normalize_custom_query_payload(raw_data):
if not isinstance(raw_data, dict):
@@ -168,8 +135,6 @@ def execute_custom_query(
cypher: str,
provider_id: str,
) -> dict[str, Any]:
validate_custom_query(cypher)
try:
graph = graph_database.execute_read_query(
database=database_name,
@@ -178,9 +143,6 @@ def execute_custom_query(
serialized = _serialize_graph(graph, provider_id)
return _truncate_graph(serialized)
except graph_database.ClientStatementException as exc:
raise ValidationError({"query": exc.message})
except graph_database.WriteQueryNotAllowedException:
raise PermissionDenied(
"Attack Paths query execution failed: read-only queries are enforced"
@@ -265,12 +227,6 @@ def _serialize_graph(graph, provider_id: str) -> dict[str, Any]:
},
)
filtered_count = len(graph.nodes) - len(nodes)
if filtered_count > 0:
logger.debug(
f"Filtered {filtered_count} nodes without matching provider_id={provider_id}"
)
relationships = []
for relationship in graph.relationships:
if relationship._properties.get("provider_id") != provider_id:
@@ -1,39 +0,0 @@
from django.db import migrations
import api.db_utils
class Migration(migrations.Migration):
dependencies = [
("api", "0083_image_provider"),
]
operations = [
migrations.AlterField(
model_name="provider",
name="provider",
field=api.db_utils.ProviderEnumField(
choices=[
("aws", "AWS"),
("azure", "Azure"),
("gcp", "GCP"),
("kubernetes", "Kubernetes"),
("m365", "M365"),
("github", "GitHub"),
("mongodbatlas", "MongoDB Atlas"),
("iac", "IaC"),
("oraclecloud", "Oracle Cloud Infrastructure"),
("alibabacloud", "Alibaba Cloud"),
("cloudflare", "Cloudflare"),
("openstack", "OpenStack"),
("image", "Image"),
("googleworkspace", "Google Workspace"),
],
default="aws",
),
),
migrations.RunSQL(
"ALTER TYPE provider ADD VALUE IF NOT EXISTS 'googleworkspace';",
reverse_sql=migrations.RunSQL.noop,
),
]
-10
View File
@@ -293,7 +293,6 @@ class Provider(RowLevelSecurityProtectedModel):
CLOUDFLARE = "cloudflare", _("Cloudflare")
OPENSTACK = "openstack", _("OpenStack")
IMAGE = "image", _("Image")
GOOGLEWORKSPACE = "googleworkspace", _("Google Workspace")
@staticmethod
def validate_aws_uid(value):
@@ -343,15 +342,6 @@ class Provider(RowLevelSecurityProtectedModel):
pointer="/data/attributes/uid",
)
@staticmethod
def validate_googleworkspace_uid(value):
if not re.match(r"^C[0-9a-zA-Z]+$", value):
raise ModelValidationError(
detail="Google Workspace Customer ID must start with 'C' followed by one or more alphanumeric characters (e.g., C01234abc, C12345678).",
code="googleworkspace-uid",
pointer="/data/attributes/uid",
)
@staticmethod
def validate_kubernetes_uid(value):
if not re.match(
File diff suppressed because it is too large Load Diff
@@ -501,72 +501,6 @@ def test_execute_custom_query_wraps_graph_errors():
mock_logger.error.assert_called_once()
# -- validate_custom_query ------------------------------------------------
@pytest.mark.parametrize(
"cypher",
[
"LOAD CSV FROM 'http://169.254.169.254/' AS x RETURN x",
"load csv from 'http://evil.com' as row return row",
"CALL apoc.load.json('http://evil.com/') YIELD value RETURN value",
"CALL apoc.load.csvParams('http://evil.com/', {}, null) YIELD list RETURN list",
"CALL apoc.import.csv([{fileName: 'f'}], [], {}) YIELD node RETURN node",
"CALL apoc.export.csv.all('file.csv', {})",
"CALL apoc.cypher.run('CREATE (n)', {}) YIELD value RETURN value",
"CALL apoc.systemdb.graph() YIELD nodes RETURN nodes",
"CALL apoc.config.list() YIELD key, value RETURN key, value",
"CALL apoc.periodic.iterate('MATCH (n) RETURN n', 'DELETE n', {batchSize: 100})",
"CALL apoc.do.when(true, 'CREATE (n) RETURN n', '', {}) YIELD value RETURN value",
"CALL apoc.trigger.add('t', 'RETURN 1', {phase: 'before'})",
"CALL apoc.custom.asProcedure('myProc', 'RETURN 1')",
],
ids=[
"LOAD_CSV",
"LOAD_CSV_lowercase",
"apoc.load.json",
"apoc.load.csvParams",
"apoc.import.csv",
"apoc.export.csv",
"apoc.cypher.run",
"apoc.systemdb.graph",
"apoc.config.list",
"apoc.periodic.iterate",
"apoc.do.when",
"apoc.trigger.add",
"apoc.custom.asProcedure",
],
)
def test_validate_custom_query_rejects_blocked_patterns(cypher):
with pytest.raises(ValidationError) as exc:
views_helpers.validate_custom_query(cypher)
assert "blocked operation" in str(exc.value.detail)
@pytest.mark.parametrize(
"cypher",
[
"MATCH (n:AWSAccount) RETURN n LIMIT 10",
"MATCH (a)-[r]->(b) RETURN a, r, b",
"MATCH (n) WHERE n.name CONTAINS 'load' RETURN n",
"CALL apoc.create.vNode(['Label'], {}) YIELD node RETURN node",
"MATCH (n) WHERE n.name = 'apoc.load.json' RETURN n",
'MATCH (n) WHERE n.description = "LOAD CSV is cool" RETURN n',
],
ids=[
"simple_match",
"traversal",
"contains_load_substring",
"apoc_virtual_node",
"apoc_load_inside_single_quotes",
"load_csv_inside_double_quotes",
],
)
def test_validate_custom_query_allows_clean_queries(cypher):
views_helpers.validate_custom_query(cypher)
# -- _truncate_graph ----------------------------------------------------------
-8
View File
@@ -23,9 +23,6 @@ from prowler.providers.azure.azure_provider import AzureProvider
from prowler.providers.cloudflare.cloudflare_provider import CloudflareProvider
from prowler.providers.gcp.gcp_provider import GcpProvider
from prowler.providers.github.github_provider import GithubProvider
from prowler.providers.googleworkspace.googleworkspace_provider import (
GoogleworkspaceProvider,
)
from prowler.providers.iac.iac_provider import IacProvider
from prowler.providers.image.image_provider import ImageProvider
from prowler.providers.kubernetes.kubernetes_provider import KubernetesProvider
@@ -116,7 +113,6 @@ class TestReturnProwlerProvider:
[
(Provider.ProviderChoices.AWS.value, AwsProvider),
(Provider.ProviderChoices.GCP.value, GcpProvider),
(Provider.ProviderChoices.GOOGLEWORKSPACE.value, GoogleworkspaceProvider),
(Provider.ProviderChoices.AZURE.value, AzureProvider),
(Provider.ProviderChoices.KUBERNETES.value, KubernetesProvider),
(Provider.ProviderChoices.M365.value, M365Provider),
@@ -252,10 +248,6 @@ class TestGetProwlerProviderKwargs:
Provider.ProviderChoices.GCP.value,
{"project_ids": ["provider_uid"]},
),
(
Provider.ProviderChoices.GOOGLEWORKSPACE.value,
{},
),
(
Provider.ProviderChoices.KUBERNETES.value,
{"context": "provider_uid"},
+14 -409
View File
@@ -1190,26 +1190,6 @@ class TestProviderViewSet:
"uid": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"alias": "OpenStack Project",
},
{
"provider": "googleworkspace",
"uid": "C01234abc",
"alias": "Google Workspace Customer",
},
{
"provider": "googleworkspace",
"uid": "C12345678",
"alias": "Google Workspace All Digits",
},
{
"provider": "googleworkspace",
"uid": "CABCDEF123",
"alias": "Google Workspace Uppercase",
},
{
"provider": "googleworkspace",
"uid": "C12",
"alias": "Google Workspace Minimum Length",
},
]
),
)
@@ -1362,11 +1342,7 @@ class TestProviderViewSet:
response = authenticated_client.post(
reverse("provider-list"), data=provider_json_payload, format="json"
)
assert response.status_code == status.HTTP_409_CONFLICT
error = response.json()["errors"][0]
assert error["detail"] == "Provider already exists."
assert error["code"] == "conflict"
assert error["source"]["pointer"] == "/data/attributes/uid"
assert response.status_code == status.HTTP_400_BAD_REQUEST
mock_delete_task.reset_mock()
mock_delete_task.return_value = task_mock
@@ -1658,36 +1634,6 @@ class TestProviderViewSet:
"min_length",
"uid",
),
# Google Workspace UID validation - missing 'C' prefix
(
{
"provider": "googleworkspace",
"uid": "01234abc",
"alias": "test",
},
"googleworkspace-uid",
"uid",
),
# Google Workspace UID validation - contains special characters
(
{
"provider": "googleworkspace",
"uid": "C0123-abc",
"alias": "test",
},
"googleworkspace-uid",
"uid",
),
# Google Workspace UID validation - lowercase 'c' prefix
(
{
"provider": "googleworkspace",
"uid": "c12345678",
"alias": "test",
},
"googleworkspace-uid",
"uid",
),
]
),
)
@@ -1861,21 +1807,21 @@ class TestProviderViewSet:
(
"uid.icontains",
"1",
11,
10,
),
("alias", "aws_testing_1", 1),
("alias.icontains", "aws", 2),
("inserted_at", TODAY, 12),
("inserted_at", TODAY, 11),
(
"inserted_at.gte",
"2024-01-01",
12,
11,
),
("inserted_at.lte", "2024-01-01", 0),
(
"updated_at.gte",
"2024-01-01",
12,
11,
),
("updated_at.lte", "2024-01-01", 0),
]
@@ -2491,15 +2437,6 @@ class TestProviderSecretViewSet:
"clouds_yaml_cloud": "mycloud",
},
),
# Google Workspace with service account credentials
(
Provider.ProviderChoices.GOOGLEWORKSPACE.value,
ProviderSecret.TypeChoices.STATIC,
{
"credentials_content": '{"type": "service_account", "project_id": "test-project", "private_key_id": "key123", "private_key": "-----BEGIN PRIVATE KEY-----\\ntest\\n-----END PRIVATE KEY-----\\n", "client_email": "test@test-project.iam.gserviceaccount.com", "client_id": "123456789"}',
"delegated_user": "admin@example.com",
},
),
],
)
def test_provider_secrets_create_valid(
@@ -3810,12 +3747,6 @@ class TestTaskViewSet:
@pytest.mark.django_db
class TestAttackPathsScanViewSet:
@pytest.fixture(autouse=True)
def _clear_throttle_cache(self):
from django.core.cache import cache
cache.clear()
@staticmethod
def _run_payload(query_id="aws-rds", parameters=None):
return {
@@ -4417,6 +4348,8 @@ class TestAttackPathsScanViewSet:
}
}
# TODO: Remove skip once queries/custom and schema endpoints are unblocked
@pytest.mark.skip(reason="Endpoint temporarily blocked")
def test_run_custom_query_returns_graph(
self,
authenticated_client,
@@ -4474,6 +4407,7 @@ class TestAttackPathsScanViewSet:
assert attributes["total_nodes"] == 1
assert attributes["truncated"] is False
@pytest.mark.skip(reason="Endpoint temporarily blocked")
def test_run_custom_query_returns_text_when_accept_text_plain(
self,
authenticated_client,
@@ -4528,6 +4462,7 @@ class TestAttackPathsScanViewSet:
assert "## Relationships (0)" in body
assert "## Summary" in body
@pytest.mark.skip(reason="Endpoint temporarily blocked")
def test_run_custom_query_returns_404_when_no_nodes(
self,
authenticated_client,
@@ -4569,6 +4504,7 @@ class TestAttackPathsScanViewSet:
assert response.status_code == status.HTTP_404_NOT_FOUND
@pytest.mark.skip(reason="Endpoint temporarily blocked")
def test_run_custom_query_returns_400_when_graph_not_ready(
self,
authenticated_client,
@@ -4595,6 +4531,7 @@ class TestAttackPathsScanViewSet:
assert response.status_code == status.HTTP_400_BAD_REQUEST
assert "not available" in response.json()["errors"][0]["detail"]
@pytest.mark.skip(reason="Endpoint temporarily blocked")
def test_run_custom_query_returns_403_for_write_query(
self,
authenticated_client,
@@ -4632,343 +4569,9 @@ class TestAttackPathsScanViewSet:
assert response.status_code == status.HTTP_403_FORBIDDEN
# -- SSRF blocklist (HTTP level) ----------------------------------------------
@pytest.mark.parametrize(
"cypher",
[
"LOAD CSV FROM 'http://169.254.169.254/' AS x RETURN x",
"CALL apoc.load.json('http://evil.com/') YIELD value RETURN value",
"CALL apoc.import.csv([{fileName: 'f'}], [], {}) YIELD node RETURN node",
"CALL apoc.export.csv.all('file.csv', {})",
"CALL apoc.cypher.run('CREATE (n)', {}) YIELD value RETURN value",
"CALL apoc.systemdb.graph() YIELD nodes RETURN nodes",
],
ids=[
"LOAD_CSV",
"apoc.load",
"apoc.import",
"apoc.export",
"apoc.cypher.run",
"apoc.systemdb",
],
)
def test_run_custom_query_rejects_ssrf_patterns(
self,
authenticated_client,
providers_fixture,
scans_fixture,
create_attack_paths_scan,
cypher,
):
provider = providers_fixture[0]
attack_paths_scan = create_attack_paths_scan(
provider,
scan=scans_fixture[0],
graph_data_ready=True,
)
with patch(
"api.v1.views.graph_database.get_database_name",
return_value="db-test",
):
response = authenticated_client.post(
reverse(
"attack-paths-scans-queries-custom",
kwargs={"pk": attack_paths_scan.id},
),
data=self._custom_query_payload(cypher),
content_type=API_JSON_CONTENT_TYPE,
)
assert response.status_code == status.HTTP_400_BAD_REQUEST
assert "blocked" in response.json()["errors"][0]["detail"].lower()
# -- Cross-tenant isolation ---------------------------------------------------
def test_run_custom_query_returns_404_for_foreign_tenant(
self,
authenticated_client,
create_attack_paths_scan,
):
from api.models import Provider, Tenant
foreign_tenant = Tenant.objects.create(name="foreign-tenant")
foreign_provider = Provider.objects.create(
tenant=foreign_tenant,
provider="aws",
uid="123456789999",
)
attack_paths_scan = create_attack_paths_scan(
foreign_provider,
graph_data_ready=True,
)
with patch(
"api.v1.views.graph_database.get_database_name",
return_value="db-test",
):
response = authenticated_client.post(
reverse(
"attack-paths-scans-queries-custom",
kwargs={"pk": attack_paths_scan.id},
),
data=self._custom_query_payload(),
content_type=API_JSON_CONTENT_TYPE,
)
assert response.status_code == status.HTTP_404_NOT_FOUND
def test_cartography_schema_returns_404_for_foreign_tenant(
self,
authenticated_client,
create_attack_paths_scan,
):
from api.models import Provider, Tenant
foreign_tenant = Tenant.objects.create(name="foreign-tenant-schema")
foreign_provider = Provider.objects.create(
tenant=foreign_tenant,
provider="aws",
uid="123456789998",
)
attack_paths_scan = create_attack_paths_scan(
foreign_provider,
graph_data_ready=True,
)
response = authenticated_client.get(
reverse(
"attack-paths-scans-schema",
kwargs={"pk": attack_paths_scan.id},
)
)
assert response.status_code == status.HTTP_404_NOT_FOUND
# -- Authentication / authorization -------------------------------------------
def test_run_custom_query_returns_401_unauthenticated(
self,
providers_fixture,
scans_fixture,
create_attack_paths_scan,
):
from rest_framework.test import APIClient
provider = providers_fixture[0]
attack_paths_scan = create_attack_paths_scan(
provider,
scan=scans_fixture[0],
graph_data_ready=True,
)
unauthenticated = APIClient()
response = unauthenticated.post(
reverse(
"attack-paths-scans-queries-custom",
kwargs={"pk": attack_paths_scan.id},
),
data=self._custom_query_payload(),
content_type=API_JSON_CONTENT_TYPE,
)
assert response.status_code == status.HTTP_401_UNAUTHORIZED
def test_cartography_schema_returns_401_unauthenticated(
self,
providers_fixture,
scans_fixture,
create_attack_paths_scan,
):
from rest_framework.test import APIClient
provider = providers_fixture[0]
attack_paths_scan = create_attack_paths_scan(
provider,
scan=scans_fixture[0],
graph_data_ready=True,
)
unauthenticated = APIClient()
response = unauthenticated.get(
reverse(
"attack-paths-scans-schema",
kwargs={"pk": attack_paths_scan.id},
)
)
assert response.status_code == status.HTTP_401_UNAUTHORIZED
def test_run_custom_query_returns_403_no_manage_scans(
self,
authenticated_client_no_permissions_rbac,
providers_fixture,
scans_fixture,
create_attack_paths_scan,
):
provider = providers_fixture[0]
attack_paths_scan = create_attack_paths_scan(
provider,
scan=scans_fixture[0],
graph_data_ready=True,
)
response = authenticated_client_no_permissions_rbac.post(
reverse(
"attack-paths-scans-queries-custom",
kwargs={"pk": attack_paths_scan.id},
),
data=self._custom_query_payload(),
content_type=API_JSON_CONTENT_TYPE,
)
assert response.status_code == status.HTTP_403_FORBIDDEN
# -- Error leakage ------------------------------------------------------------
def test_run_custom_query_does_not_leak_internals_on_error(
self,
authenticated_client,
providers_fixture,
scans_fixture,
create_attack_paths_scan,
):
from rest_framework.exceptions import APIException
provider = providers_fixture[0]
attack_paths_scan = create_attack_paths_scan(
provider,
scan=scans_fixture[0],
graph_data_ready=True,
)
with (
patch(
"api.v1.views.attack_paths_views_helpers.execute_custom_query",
side_effect=APIException(
"Attack Paths query execution failed due to a database error"
),
),
patch(
"api.v1.views.graph_database.get_database_name",
return_value="db-test",
),
):
response = authenticated_client.post(
reverse(
"attack-paths-scans-queries-custom",
kwargs={"pk": attack_paths_scan.id},
),
data=self._custom_query_payload(),
content_type=API_JSON_CONTENT_TYPE,
)
assert response.status_code == status.HTTP_500_INTERNAL_SERVER_ERROR
body = json.dumps(response.json()).lower()
for forbidden_term in ["neo4j", "bolt://", "syntaxerror", "db-tenant-"]:
assert forbidden_term not in body
# -- Rate limiting (throttle) -------------------------------------------------
def test_run_custom_query_throttled_after_limit(
self,
authenticated_client,
providers_fixture,
scans_fixture,
create_attack_paths_scan,
):
provider = providers_fixture[0]
attack_paths_scan = create_attack_paths_scan(
provider,
scan=scans_fixture[0],
graph_data_ready=True,
)
mock_graph = {
"nodes": [{"id": "n1", "labels": ["Test"], "properties": {}}],
"relationships": [],
"total_nodes": 1,
"truncated": False,
}
url = reverse(
"attack-paths-scans-queries-custom",
kwargs={"pk": attack_paths_scan.id},
)
payload = self._custom_query_payload()
with (
patch(
"api.v1.views.attack_paths_views_helpers.execute_custom_query",
return_value=mock_graph,
),
patch(
"api.v1.views.graph_database.get_database_name",
return_value="db-test",
),
patch(
"api.v1.views.graph_database.clear_cache",
),
):
for i in range(11):
response = authenticated_client.post(
url,
data=payload,
content_type=API_JSON_CONTENT_TYPE,
)
if i < 10:
assert (
response.status_code == status.HTTP_200_OK
), f"Request {i + 1} should succeed with 200 OK, got {response.status_code}"
else:
assert (
response.status_code == status.HTTP_429_TOO_MANY_REQUESTS
), f"Request {i + 1} should be throttled"
# -- Timeout simulation -------------------------------------------------------
def test_run_custom_query_returns_500_on_database_timeout(
self,
authenticated_client,
providers_fixture,
scans_fixture,
create_attack_paths_scan,
):
from rest_framework.exceptions import APIException
provider = providers_fixture[0]
attack_paths_scan = create_attack_paths_scan(
provider,
scan=scans_fixture[0],
graph_data_ready=True,
)
with (
patch(
"api.v1.views.attack_paths_views_helpers.execute_custom_query",
side_effect=APIException(
"Attack Paths query execution failed due to a database error"
),
),
patch(
"api.v1.views.graph_database.get_database_name",
return_value="db-test",
),
):
response = authenticated_client.post(
reverse(
"attack-paths-scans-queries-custom",
kwargs={"pk": attack_paths_scan.id},
),
data=self._custom_query_payload(),
content_type=API_JSON_CONTENT_TYPE,
)
assert response.status_code == status.HTTP_500_INTERNAL_SERVER_ERROR
# -- cartography_schema action ------------------------------------------------
@pytest.mark.skip(reason="Endpoint temporarily blocked")
def test_cartography_schema_returns_urls(
self,
authenticated_client,
@@ -5018,6 +4621,7 @@ class TestAttackPathsScanViewSet:
assert "schema.md" in attributes["schema_url"]
assert "raw.githubusercontent.com" in attributes["raw_schema_url"]
@pytest.mark.skip(reason="Endpoint temporarily blocked")
def test_cartography_schema_returns_404_when_no_metadata(
self,
authenticated_client,
@@ -5052,6 +4656,7 @@ class TestAttackPathsScanViewSet:
assert response.status_code == status.HTTP_404_NOT_FOUND
assert "No cartography schema metadata" in str(response.json())
@pytest.mark.skip(reason="Endpoint temporarily blocked")
def test_cartography_schema_returns_400_when_graph_not_ready(
self,
authenticated_client,
+2 -13
View File
@@ -27,9 +27,6 @@ if TYPE_CHECKING:
from prowler.providers.cloudflare.cloudflare_provider import CloudflareProvider
from prowler.providers.gcp.gcp_provider import GcpProvider
from prowler.providers.github.github_provider import GithubProvider
from prowler.providers.googleworkspace.googleworkspace_provider import (
GoogleworkspaceProvider,
)
from prowler.providers.iac.iac_provider import IacProvider
from prowler.providers.image.image_provider import ImageProvider
from prowler.providers.kubernetes.kubernetes_provider import KubernetesProvider
@@ -86,7 +83,6 @@ def return_prowler_provider(
| CloudflareProvider
| GcpProvider
| GithubProvider
| GoogleworkspaceProvider
| IacProvider
| ImageProvider
| KubernetesProvider
@@ -101,7 +97,7 @@ def return_prowler_provider(
provider (Provider): The provider object containing the provider type and associated secrets.
Returns:
AlibabacloudProvider | AwsProvider | AzureProvider | CloudflareProvider | GcpProvider | GithubProvider | GoogleworkspaceProvider | IacProvider | ImageProvider | KubernetesProvider | M365Provider | MongodbatlasProvider | OpenstackProvider | OraclecloudProvider: The corresponding provider class.
AlibabacloudProvider | AwsProvider | AzureProvider | CloudflareProvider | GcpProvider | GithubProvider | IacProvider | ImageProvider | KubernetesProvider | M365Provider | MongodbatlasProvider | OpenstackProvider | OraclecloudProvider: The corresponding provider class.
Raises:
ValueError: If the provider type specified in `provider.provider` is not supported.
@@ -115,12 +111,6 @@ def return_prowler_provider(
from prowler.providers.gcp.gcp_provider import GcpProvider
prowler_provider = GcpProvider
case Provider.ProviderChoices.GOOGLEWORKSPACE.value:
from prowler.providers.googleworkspace.googleworkspace_provider import (
GoogleworkspaceProvider,
)
prowler_provider = GoogleworkspaceProvider
case Provider.ProviderChoices.AZURE.value:
from prowler.providers.azure.azure_provider import AzureProvider
@@ -273,7 +263,6 @@ def initialize_prowler_provider(
| CloudflareProvider
| GcpProvider
| GithubProvider
| GoogleworkspaceProvider
| IacProvider
| ImageProvider
| KubernetesProvider
@@ -289,7 +278,7 @@ def initialize_prowler_provider(
mutelist_processor (Processor): The mutelist processor object containing the mutelist configuration.
Returns:
AlibabacloudProvider | AwsProvider | AzureProvider | CloudflareProvider | GcpProvider | GithubProvider | GoogleworkspaceProvider | IacProvider | ImageProvider | KubernetesProvider | M365Provider | MongodbatlasProvider | OpenstackProvider | OraclecloudProvider: An instance of the corresponding provider class
AlibabacloudProvider | AwsProvider | AzureProvider | CloudflareProvider | GcpProvider | GithubProvider | IacProvider | ImageProvider | KubernetesProvider | M365Provider | MongodbatlasProvider | OpenstackProvider | OraclecloudProvider: An instance of the corresponding provider class
initialized with the provider's secrets.
"""
prowler_provider = return_prowler_provider(provider)
@@ -191,22 +191,6 @@ from rest_framework_json_api import serializers
},
"required": ["service_account_key"],
},
{
"type": "object",
"title": "Google Workspace Service Account",
"properties": {
"credentials_content": {
"type": "string",
"description": "The service account JSON credentials content for Google Workspace API access with domain-wide delegation enabled.",
},
"delegated_user": {
"type": "string",
"format": "email",
"description": "The email address of the Google Workspace super admin user to impersonate for domain-wide delegation.",
},
},
"required": ["credentials_content", "delegated_user"],
},
{
"type": "object",
"title": "Kubernetes Static Credentials",
+1 -32
View File
@@ -6,7 +6,6 @@ from django.conf import settings
from django.contrib.auth import authenticate
from django.contrib.auth.models import update_last_login
from django.contrib.auth.password_validation import validate_password
from django.core.exceptions import ValidationError as DjangoValidationError
from django.db import IntegrityError
from drf_spectacular.utils import extend_schema_field
from jwt.exceptions import InvalidKeyError
@@ -960,26 +959,6 @@ class ProviderCreateSerializer(RLSSerializer, BaseWriteSerializer):
},
}
def create(self, validated_data):
try:
return super().create(validated_data)
except DjangoValidationError as e:
if "unique_provider_uids" in str(e):
raise ConflictException(
detail="Provider already exists.",
pointer="/data/attributes/uid",
)
raise
except IntegrityError as e:
# Handle race conditions where the unique constraint is enforced at the DB level
# after validation has already passed.
if "unique_provider_uids" in str(e):
raise ConflictException(
detail="Provider already exists.",
pointer="/data/attributes/uid",
)
raise
class ProviderUpdateSerializer(BaseWriteSerializer):
"""
@@ -1241,7 +1220,7 @@ class AttackPathsQueryRunRequestSerializer(BaseSerializerV1):
class AttackPathsCustomQueryRunRequestSerializer(BaseSerializerV1):
query = serializers.CharField(max_length=10000, min_length=1, trim_whitespace=True)
query = serializers.CharField()
class JSONAPIMeta:
resource_name = "attack-paths-custom-query-run-requests"
@@ -1541,8 +1520,6 @@ class BaseWriteProviderSecretSerializer(BaseWriteSerializer):
serializer = AzureProviderSecret(data=secret)
elif provider_type == Provider.ProviderChoices.GCP.value:
serializer = GCPProviderSecret(data=secret)
elif provider_type == Provider.ProviderChoices.GOOGLEWORKSPACE.value:
serializer = GoogleWorkspaceProviderSecret(data=secret)
elif provider_type == Provider.ProviderChoices.GITHUB.value:
serializer = GithubProviderSecret(data=secret)
elif provider_type == Provider.ProviderChoices.IAC.value:
@@ -1678,14 +1655,6 @@ class GCPServiceAccountProviderSecret(serializers.Serializer):
resource_name = "provider-secrets"
class GoogleWorkspaceProviderSecret(serializers.Serializer):
credentials_content = serializers.CharField()
delegated_user = serializers.EmailField()
class Meta:
resource_name = "provider-secrets"
class MongoDBAtlasProviderSecret(serializers.Serializer):
atlas_public_key = serializers.CharField()
atlas_private_key = serializers.CharField()
+11 -7
View File
@@ -51,13 +51,6 @@ from api.v1.views import (
)
# This helper view is used to block any endpoints that should not be available
# To use it, add a new entry in the `urlpatterns` list, for example (old but real one):
# path(
# "attack-paths-scans/<uuid:pk>/queries/custom",
# _blocked_endpoint,
# name="attack-paths-scans-queries-custom-blocked",
# ),
@csrf_exempt
def _blocked_endpoint(request, *args, **kwargs):
return JsonResponse(
@@ -216,6 +209,17 @@ urlpatterns = [
path("tokens/saml", SAMLTokenValidateView.as_view(), name="token-saml"),
path("tokens/google", GoogleSocialLoginView.as_view(), name="token-google"),
path("tokens/github", GithubSocialLoginView.as_view(), name="token-github"),
# TODO: Remove these blocked endpoints once they are properly tested
path(
"attack-paths-scans/<uuid:pk>/queries/custom",
_blocked_endpoint,
name="attack-paths-scans-queries-custom-blocked",
),
path(
"attack-paths-scans/<uuid:pk>/schema",
_blocked_endpoint,
name="attack-paths-scans-schema-blocked",
),
path("", include(router.urls)),
path("", include(tenants_router.urls)),
path("", include(users_router.urls)),
+1 -50
View File
@@ -3,7 +3,6 @@ import glob
import json
import logging
import os
import time
from collections import defaultdict
from copy import deepcopy
@@ -408,7 +407,7 @@ class SchemaView(SpectacularAPIView):
def get(self, request, *args, **kwargs):
spectacular_settings.TITLE = "Prowler API"
spectacular_settings.VERSION = "1.21.1"
spectacular_settings.VERSION = "1.20.0"
spectacular_settings.DESCRIPTION = (
"Prowler API specification.\n\nThis file is auto-generated."
)
@@ -2452,11 +2451,6 @@ class AttackPathsScanViewSet(BaseRLSViewSet):
# RBAC required permissions
required_permissions = [Permissions.MANAGE_SCANS]
def get_throttles(self):
if self.action == "run_custom_attack_paths_query":
self.throttle_scope = "attack-paths-custom-query"
return super().get_throttles()
def set_required_permissions(self):
if self.request.method in SAFE_METHODS:
self.required_permissions = []
@@ -2576,35 +2570,14 @@ class AttackPathsScanViewSet(BaseRLSViewSet):
provider_id,
)
start = time.monotonic()
graph = attack_paths_views_helpers.execute_query(
database_name,
query_definition,
parameters,
provider_id,
)
query_duration = time.monotonic() - start
graph_database.clear_cache(database_name)
result_nodes = len(graph.get("nodes", []))
result_relationships = len(graph.get("relationships", []))
logger.info(
"attack_paths_query_run",
extra={
"user_id": str(request.user.id),
"tenant_id": str(attack_paths_scan.provider.tenant_id),
"metadata": {
"query_id": query_definition.id,
"provider": query_definition.provider,
"scan_id": pk,
"provider_id": provider_id,
"result_nodes": result_nodes,
"result_relationships": result_relationships,
"query_duration": round(query_duration, 3),
},
},
)
status_code = status.HTTP_200_OK
if not graph.get("nodes"):
status_code = status.HTTP_404_NOT_FOUND
@@ -2645,35 +2618,13 @@ class AttackPathsScanViewSet(BaseRLSViewSet):
)
provider_id = str(attack_paths_scan.provider_id)
start = time.monotonic()
graph = attack_paths_views_helpers.execute_custom_query(
database_name,
serializer.validated_data["query"],
provider_id,
)
query_duration = time.monotonic() - start
graph_database.clear_cache(database_name)
query_length = len(serializer.validated_data["query"])
result_nodes = len(graph.get("nodes", []))
result_relationships = len(graph.get("relationships", []))
logger.info(
"attack_paths_custom_query_run",
extra={
"user_id": str(request.user.id),
"tenant_id": str(attack_paths_scan.provider.tenant_id),
"metadata": {
"provider": attack_paths_scan.provider.provider,
"scan_id": pk,
"provider_id": provider_id,
"query_length": query_length,
"result_nodes": result_nodes,
"result_relationships": result_relationships,
"query_duration": round(query_duration, 3),
},
},
)
status_code = status.HTTP_200_OK
if not graph.get("nodes"):
status_code = status.HTTP_404_NOT_FOUND
-5
View File
@@ -2,7 +2,6 @@ import json
import logging
from enum import StrEnum
from config.env import env
from django_guid.log_filters import CorrelationId
@@ -63,8 +62,6 @@ class NDJSONFormatter(logging.Formatter):
log_record["duration"] = record.duration
if hasattr(record, "status_code"):
log_record["status_code"] = record.status_code
if hasattr(record, "metadata"):
log_record["metadata"] = record.metadata
if record.exc_info:
log_record["exc_info"] = self.formatException(record.exc_info)
@@ -110,8 +107,6 @@ class HumanReadableFormatter(logging.Formatter):
log_components.append(f"done in {record.duration}s:")
if hasattr(record, "status_code"):
log_components.append(f"{record.status_code}")
if hasattr(record, "metadata"):
log_components.append(f"metadata={record.metadata}")
if record.exc_info:
log_components.append(self.formatException(record.exc_info))
+1 -4
View File
@@ -113,11 +113,8 @@ REST_FRAMEWORK = {
"rest_framework.throttling.ScopedRateThrottle",
],
"DEFAULT_THROTTLE_RATES": {
"dj_rest_auth": None,
"token-obtain": env("DJANGO_THROTTLE_TOKEN_OBTAIN", default=None),
"attack-paths-custom-query": env(
"DJANGO_THROTTLE_ATTACK_PATHS_CUSTOM_QUERY", default="10/min"
),
"dj_rest_auth": None,
},
}
-7
View File
@@ -543,12 +543,6 @@ def providers_fixture(tenants_fixture):
alias="openstack_testing",
tenant_id=tenant.id,
)
provider12 = Provider.objects.create(
provider="googleworkspace",
uid="C12345678",
alias="googleworkspace_testing",
tenant_id=tenant.id,
)
return (
provider1,
@@ -562,7 +556,6 @@ def providers_fixture(tenants_fixture):
provider9,
provider10,
provider11,
provider12,
)
+2 -28
View File
@@ -43,7 +43,6 @@ def start_aws_ingestion(
"aws_guardduty_severity_threshold": cartography_config.aws_guardduty_severity_threshold,
"aws_cloudtrail_management_events_lookback_hours": cartography_config.aws_cloudtrail_management_events_lookback_hours,
"experimental_aws_inspector_batch": cartography_config.experimental_aws_inspector_batch,
"aws_tagging_api_cleanup_batch": cartography_config.aws_tagging_api_cleanup_batch,
}
boto3_session = get_boto3_session(prowler_api_provider, prowler_sdk_provider)
@@ -117,30 +116,6 @@ def start_aws_ingestion(
neo4j_session,
common_job_parameters,
)
if all(
s in requested_syncs
for s in ["ecs", "ec2:load_balancer_v2", "ec2:load_balancer_v2:expose"]
):
logger.info(
f"Syncing lb_container_exposure scoped analysis for AWS account {prowler_api_provider.uid}"
)
cartography_aws.run_scoped_analysis_job(
"aws_lb_container_exposure.json",
neo4j_session,
common_job_parameters,
)
if all(s in requested_syncs for s in ["ec2:network_acls", "ec2:load_balancer_v2"]):
logger.info(
f"Syncing lb_nacl_direct scoped analysis for AWS account {prowler_api_provider.uid}"
)
cartography_aws.run_scoped_analysis_job(
"aws_lb_nacl_direct.json",
neo4j_session,
common_job_parameters,
)
db_utils.update_attack_paths_scan_progress(attack_paths_scan, 91)
logger.info(f"Syncing metadata for AWS account {prowler_api_provider.uid}")
@@ -264,9 +239,8 @@ def sync_aws_account(
failed_syncs[func_name] = exception_message
logger.warning(
f"Caught exception syncing function {func_name} from AWS account {prowler_api_provider.uid}: {e}. "
"Continuing to the next AWS sync function.",
exc_info=True,
f"Caught exception syncing function {func_name} from AWS account {prowler_api_provider.uid}. We "
"are continuing on to the next AWS sync function.",
)
continue
@@ -1,58 +1,3 @@
"""
Attack Paths scan orchestrator.
Runs the full scan lifecycle for a single provider, called from a Celery task.
The idea is simple: ingest everything into a throwaway Neo4j database, enrich
it with Prowler-specific data, then swap it into the tenant's long-lived
database so queries never see a half-built graph.
Two databases are involved:
- Temporary (db-tmp-scan-<attack_paths_scan_id>): short-lived, single-provider, dropped after sync.
- Tenant (db-tenant-<tenant_uuid>): long-lived, multi-provider, what the API queries against.
Pipeline steps:
1. Resolve the Prowler provider and SDK credentials from the scan ID.
Retrieve or create the AttackPathsScan row. Exit early if the provider
type has no ingestion function (only AWS is supported today).
2. Create a fresh temporary Neo4j database and set up Cartography indexes
plus ProwlerFinding indexes before writing any data.
3. Run the provider-specific Cartography ingestion (e.g. aws.start_aws_ingestion).
This iterates over cloud services and writes the standard Cartography nodes
(AWSAccount, EC2Instance, IAMRole, etc.) and relationships (RESOURCE,
POLICY, STATEMENT, TRUSTS_AWS_PRINCIPAL, ...) into the temp database.
Wrapped in call_within_event_loop because some Cartography modules use async.
4. Run Cartography post-processing: ontology for label propagation and
analysis for derived relationships.
5. Create an Internet singleton node and add CAN_ACCESS relationships to
internet-exposed resources (EC2Instance, LoadBalancer, LoadBalancerV2).
6. Stream Prowler findings from Postgres in batches. Each finding becomes a
ProwlerFinding node linked to its cloud-resource node via HAS_FINDING.
Before that, an _AWSResource label (provider-specific) is added to all
nodes connected to the AWSAccount so finding lookups can use an index.
Stale findings from previous scans are cleaned up.
7. Sync the temp database into the tenant database:
- Drop the old provider subgraph (matched by _provider_id property).
graph_data_ready is set to False for all scans of this provider while
the swap happens so the API doesn't serve partial data.
- Copy nodes and relationships in batches. Every synced node gets a
_ProviderResource label and _provider_id / _provider_element_id
properties for multi-provider isolation.
- Set graph_data_ready back to True.
8. Drop the temporary database, mark the AttackPathsScan as COMPLETED.
On failure the temp database is dropped, the scan is marked FAILED, and the
exception propagates to Celery.
"""
import logging
import time
@@ -267,20 +212,18 @@ def run(tenant_id: str, scan_id: str, task_id: str) -> dict[str, Any]:
try:
graph_database.drop_database(tmp_cartography_config.neo4j_database)
except Exception as e:
except Exception:
logger.error(
f"Failed to drop temporary Neo4j database {tmp_cartography_config.neo4j_database} during cleanup: {e}",
exc_info=True,
f"Failed to drop temporary Neo4j database {tmp_cartography_config.neo4j_database} during cleanup"
)
try:
db_utils.finish_attack_paths_scan(
attack_paths_scan, StateChoices.FAILED, ingestion_exceptions
)
except Exception as e:
logger.error(
f"Could not mark attack paths scan {attack_paths_scan.id} as FAILED (row may have been deleted): {e}",
exc_info=True,
except Exception:
logger.warning(
f"Could not mark attack paths scan {attack_paths_scan.id} as FAILED (row may have been deleted)"
)
raise
+6 -11
View File
@@ -336,6 +336,7 @@ class ENSReportGenerator(BaseComplianceReportGenerator):
for req in data.requirements:
if req.status == StatusChoices.MANUAL:
continue
m = get_requirement_metadata(req.id, data.attributes_by_requirement_id)
if m:
marco = getattr(m, "Marco", "Otros")
@@ -364,12 +365,9 @@ class ENSReportGenerator(BaseComplianceReportGenerator):
elements.append(Paragraph(f"{categoria_name}", self.styles["h3"]))
for req in reqs:
if req["status"] == StatusChoices.PASS:
status_indicator = ""
elif req["status"] == StatusChoices.MANUAL:
status_indicator = ""
else:
status_indicator = ""
status_indicator = (
"" if req["status"] == StatusChoices.PASS else ""
)
nivel_badge = f"[{req['nivel'].upper()}]" if req["nivel"] else ""
elements.append(
Paragraph(
@@ -843,14 +841,11 @@ class ENSReportGenerator(BaseComplianceReportGenerator):
elements.append(Spacer(1, 0.15 * inch))
# Status and Nivel badges row
status_text = str(req.status).upper()
status_color = (
COLOR_HIGH_RISK if req.status == StatusChoices.FAIL else COLOR_GRAY
)
status_color = COLOR_HIGH_RISK # FAIL
nivel_color = nivel_colors.get(nivel, COLOR_GRAY)
badges_row1 = [
["State:", status_text, "", f"Nivel: {nivel.upper()}"],
["State:", "FAIL", "", f"Nivel: {nivel.upper()}"],
]
badges_table1 = Table(
badges_row1,
@@ -35,27 +35,19 @@ def _aggregate_requirement_statistics_from_database(
}
"""
requirement_statistics_by_check_id = {}
# TODO: take into account that now the relation is 1 finding == 1 resource, review this when the logic changes
with rls_transaction(tenant_id, using=READ_REPLICA_ALIAS):
aggregated_statistics_queryset = (
Finding.all_objects.filter(
tenant_id=tenant_id,
scan_id=scan_id,
muted=False,
resources__provider__is_deleted=False,
tenant_id=tenant_id, scan_id=scan_id, muted=False
)
.values("check_id")
.annotate(
total_findings=Count(
"id",
distinct=True,
filter=Q(status__in=[StatusChoices.PASS, StatusChoices.FAIL]),
),
passed_findings=Count(
"id",
distinct=True,
filter=Q(status=StatusChoices.PASS),
),
passed_findings=Count("id", filter=Q(status=StatusChoices.PASS)),
)
)
+84 -125
View File
@@ -29,7 +29,7 @@ from tasks.jobs.threatscore_utils import (
_load_findings_for_requirement_checks,
)
from api.models import Finding, Resource, ResourceFindingMapping, StatusChoices
from api.models import Finding, StatusChoices
from prowler.lib.check.models import Severity
matplotlib.use("Agg") # Use non-interactive backend for tests
@@ -39,50 +39,43 @@ matplotlib.use("Agg") # Use non-interactive backend for tests
class TestAggregateRequirementStatistics:
"""Test suite for _aggregate_requirement_statistics_from_database function."""
def _create_finding_with_resource(
self, tenant, scan, uid, check_id, status, severity=Severity.high
):
"""Helper to create a finding linked to a resource (matching scan processing behavior)."""
finding = Finding.objects.create(
tenant_id=tenant.id,
scan=scan,
uid=uid,
check_id=check_id,
status=status,
severity=severity,
impact=severity,
check_metadata={},
raw_result={},
)
resource = Resource.objects.create(
tenant_id=tenant.id,
provider=scan.provider,
uid=f"resource-{uid}",
name=f"resource-{uid}",
region="us-east-1",
service="test",
type="test::resource",
)
ResourceFindingMapping.objects.create(
tenant_id=tenant.id,
finding=finding,
resource=resource,
)
return finding
def test_aggregates_findings_correctly(self, tenants_fixture, scans_fixture):
"""Verify correct pass/total counts per check are aggregated from database."""
tenant = tenants_fixture[0]
scan = scans_fixture[0]
self._create_finding_with_resource(
tenant, scan, "finding-1", "check_1", StatusChoices.PASS
Finding.objects.create(
tenant_id=tenant.id,
scan=scan,
uid="finding-1",
check_id="check_1",
status=StatusChoices.PASS,
severity=Severity.high,
impact=Severity.high,
check_metadata={},
raw_result={},
)
self._create_finding_with_resource(
tenant, scan, "finding-2", "check_1", StatusChoices.FAIL
Finding.objects.create(
tenant_id=tenant.id,
scan=scan,
uid="finding-2",
check_id="check_1",
status=StatusChoices.FAIL,
severity=Severity.high,
impact=Severity.high,
check_metadata={},
raw_result={},
)
self._create_finding_with_resource(
tenant, scan, "finding-3", "check_2", StatusChoices.PASS, Severity.medium
Finding.objects.create(
tenant_id=tenant.id,
scan=scan,
uid="finding-3",
check_id="check_2",
status=StatusChoices.PASS,
severity=Severity.medium,
impact=Severity.medium,
check_metadata={},
raw_result={},
)
result = _aggregate_requirement_statistics_from_database(
@@ -113,73 +106,17 @@ class TestAggregateRequirementStatistics:
tenant = tenants_fixture[0]
scan = scans_fixture[0]
self._create_finding_with_resource(
tenant, scan, "finding-1", "check_1", StatusChoices.FAIL
Finding.objects.create(
tenant_id=tenant.id,
scan=scan,
uid="finding-1",
check_id="check_1",
status=StatusChoices.FAIL,
severity=Severity.high,
impact=Severity.high,
check_metadata={},
raw_result={},
)
self._create_finding_with_resource(
tenant, scan, "finding-2", "check_1", StatusChoices.FAIL
)
result = _aggregate_requirement_statistics_from_database(
str(tenant.id), str(scan.id)
)
assert result["check_1"]["passed"] == 0
assert result["check_1"]["total"] == 2
def test_multiple_findings_same_check(self, tenants_fixture, scans_fixture):
"""Verify multiple findings for same check are correctly aggregated."""
tenant = tenants_fixture[0]
scan = scans_fixture[0]
for i in range(5):
self._create_finding_with_resource(
tenant,
scan,
f"finding-{i}",
"check_1",
StatusChoices.PASS if i % 2 == 0 else StatusChoices.FAIL,
)
result = _aggregate_requirement_statistics_from_database(
str(tenant.id), str(scan.id)
)
assert result["check_1"]["passed"] == 3
assert result["check_1"]["total"] == 5
def test_mixed_statuses(self, tenants_fixture, scans_fixture):
"""Verify MANUAL status is not counted in total or passed."""
tenant = tenants_fixture[0]
scan = scans_fixture[0]
self._create_finding_with_resource(
tenant, scan, "finding-1", "check_1", StatusChoices.PASS
)
self._create_finding_with_resource(
tenant, scan, "finding-2", "check_1", StatusChoices.MANUAL
)
result = _aggregate_requirement_statistics_from_database(
str(tenant.id), str(scan.id)
)
# MANUAL findings are excluded from the aggregation query
# since it only counts PASS and FAIL statuses
assert result["check_1"]["passed"] == 1
assert result["check_1"]["total"] == 1
def test_excludes_findings_without_resources(self, tenants_fixture, scans_fixture):
"""Verify findings without resources are excluded from aggregation."""
tenant = tenants_fixture[0]
scan = scans_fixture[0]
# Finding WITH resource → should be counted
self._create_finding_with_resource(
tenant, scan, "finding-1", "check_1", StatusChoices.PASS
)
# Finding WITHOUT resource → should be EXCLUDED
Finding.objects.create(
tenant_id=tenant.id,
scan=scan,
@@ -196,15 +133,40 @@ class TestAggregateRequirementStatistics:
str(tenant.id), str(scan.id)
)
assert result["check_1"]["passed"] == 1
assert result["check_1"]["total"] == 1
assert result["check_1"]["passed"] == 0
assert result["check_1"]["total"] == 2
def test_multiple_resources_no_double_count(self, tenants_fixture, scans_fixture):
"""Verify a finding with multiple resources is only counted once."""
def test_multiple_findings_same_check(self, tenants_fixture, scans_fixture):
"""Verify multiple findings for same check are correctly aggregated."""
tenant = tenants_fixture[0]
scan = scans_fixture[0]
finding = Finding.objects.create(
for i in range(5):
Finding.objects.create(
tenant_id=tenant.id,
scan=scan,
uid=f"finding-{i}",
check_id="check_1",
status=StatusChoices.PASS if i % 2 == 0 else StatusChoices.FAIL,
severity=Severity.high,
impact=Severity.high,
check_metadata={},
raw_result={},
)
result = _aggregate_requirement_statistics_from_database(
str(tenant.id), str(scan.id)
)
assert result["check_1"]["passed"] == 3
assert result["check_1"]["total"] == 5
def test_mixed_statuses(self, tenants_fixture, scans_fixture):
"""Verify MANUAL status is counted in total but not passed."""
tenant = tenants_fixture[0]
scan = scans_fixture[0]
Finding.objects.create(
tenant_id=tenant.id,
scan=scan,
uid="finding-1",
@@ -215,27 +177,24 @@ class TestAggregateRequirementStatistics:
check_metadata={},
raw_result={},
)
# Link two resources to the same finding
for i in range(2):
resource = Resource.objects.create(
tenant_id=tenant.id,
provider=scan.provider,
uid=f"resource-{i}",
name=f"resource-{i}",
region="us-east-1",
service="test",
type="test::resource",
)
ResourceFindingMapping.objects.create(
tenant_id=tenant.id,
finding=finding,
resource=resource,
)
Finding.objects.create(
tenant_id=tenant.id,
scan=scan,
uid="finding-2",
check_id="check_1",
status=StatusChoices.MANUAL,
severity=Severity.high,
impact=Severity.high,
check_metadata={},
raw_result={},
)
result = _aggregate_requirement_statistics_from_database(
str(tenant.id), str(scan.id)
)
# MANUAL findings are excluded from the aggregation query
# since it only counts PASS and FAIL statuses
assert result["check_1"]["passed"] == 1
assert result["check_1"]["total"] == 1
-1
View File
@@ -1 +0,0 @@
charts/
-2
View File
@@ -13,8 +13,6 @@ keywords:
- gcp
- kubernetes
maintainers:
- name: Dani
email: andre.gomes@promptlyhealth.com
- name: Mihai
email: mihai.legat@gmail.com
dependencies:
+3 -3
View File
@@ -558,9 +558,9 @@ neo4j:
# Neo4j Configuration (yaml format)
config:
dbms_security_procedures_allowlist: "apoc.*"
dbms_security_procedures_unrestricted: ""
dbms_security_procedures_unrestricted: "apoc.*"
apoc_config:
apoc.export.file.enabled: "false"
apoc.import.file.enabled: "false"
apoc.export.file.enabled: "true"
apoc.import.file.enabled: "true"
apoc.import.file.use_neo4j_config: "true"
+1 -1
View File
@@ -21,7 +21,7 @@ print(
f"{Fore.GREEN}Loading all CSV files from the folder {folder_path_overview} ...\n{Style.RESET_ALL}"
)
cli.show_server_banner = lambda *x: click.echo(
f"{Fore.YELLOW}NOTE:{Style.RESET_ALL} If you are using {Fore.GREEN}{Style.BRIGHT}Prowler Cloud{Style.RESET_ALL} with the S3 integration or that integration \nfrom {Fore.CYAN}{Style.BRIGHT}Prowler CLI{Style.RESET_ALL} and you want to use your data from your S3 bucket,\nrun: `{orange_color}aws s3 cp s3://<your-bucket>/output/csv ./output --recursive{Style.RESET_ALL}`\nand then run `prowler dashboard` again to load the new files."
f"{Fore.YELLOW}NOTE:{Style.RESET_ALL} If you are using {Fore.GREEN}{Style.BRIGHT}Prowler SaaS{Style.RESET_ALL} with the S3 integration or that integration \nfrom {Fore.CYAN}{Style.BRIGHT}Prowler Open Source{Style.RESET_ALL} and you want to use your data from your S3 bucket,\nrun: `{orange_color}aws s3 cp s3://<your-bucket>/output/csv ./output --recursive{Style.RESET_ALL}`\nand then run `prowler dashboard` again to load the new files."
)
# Initialize the app - incorporate css
-1
View File
@@ -315,7 +315,6 @@ The type of resource being audited. This field helps categorize and organize fin
- **Kubernetes**: Use types shown under `KIND` from `kubectl api-resources`.
- **Oracle Cloud Infrastructure**: Use types from [Oracle Cloud Infrastructure documentation](https://docs.public.oneportal.content.oci.oraclecloud.com/en-us/iaas/Content/Search/Tasks/queryingresources_topic-Listing_Supported_Resource_Types.htm).
- **OpenStack**: Use types from [OpenStack Heat resource types](https://docs.openstack.org/heat/latest/template_guide/openstack.html).
- **Alibaba Cloud**: Use types from [Alibaba Cloud ROS resource types](https://www.alibabacloud.com/help/en/ros/developer-reference/list-of-resource-types-by-service).
- **Any other provider**: Use `NotDefined` due to lack of standardized resource types in their SDK or documentation.
#### ResourceGroup
-34
View File
@@ -3406,40 +3406,6 @@ Use existing providers as templates, this will help you to understand better the
- **Use Rules**: Use rules to ensure the code generated by AI is following the way of working in Prowler.
---
## OCSF Field Requirements for Prowler Cloud Integration
When implementing a new provider that supports the `--push-to-cloud` feature, specific OCSF fields must be correctly populated to ensure proper findings ingestion into Prowler Cloud.
### Required OCSF Fields
The following fields in the OCSF output are critical for successful ingestion:
| Field | Requirement | Description |
|-------|-------------|-------------|
| `provider_uid` | Must match the UID used when registering the provider in the API | This identifier links findings to the correct provider in Prowler Cloud |
| `provider` | Must be the provider name | The name of the provider (e.g., `aws`, `azure`, `gcp`, `googleworkspace`) |
| `finding_info.uid` | Must be unique | Each finding must have a unique identifier to avoid duplicates |
| `resources.uid` | Must have a value | The resource UID cannot be empty; it identifies the specific resource being assessed |
### Implementation Reference
These fields are set in the OCSF output generation. See the [OCSF output implementation](https://github.com/prowler-cloud/prowler/blob/master/prowler/lib/outputs/ocsf/ocsf.py) for reference.
### Validation Checklist
Before releasing a new provider with `--push-to-cloud` support:
- [ ] Verify `provider_uid` matches the UID used in the API to register the provider
- [ ] Confirm `provider` field contains the correct provider name
- [ ] Ensure all `finding_info.uid` values are unique across findings
- [ ] Validate that `resources.uid` is populated for every finding
<Tip>
Use `python scripts/validate_ocsf_output.py output/*.ocsf.json` to automate these checks.
</Tip>
## Checklist for New Providers
### CLI Integration Only
@@ -1,315 +0,0 @@
---
title: 'Test Impact Analysis'
---
Test impact analysis (TIA) determines which tests to run based on the files changed in a pull request. Instead of running the full test suite on every pull request, TIA maps changed files to the specific Prowler SDK, API, and end-to-end (E2E) tests that cover them. This approach reduces continuous integration (CI) time and resource usage while maintaining confidence that relevant code paths are tested.
## Architecture
### Components
| Component | Path | Role |
|-----------|------|------|
| Configuration | `.github/test-impact.yml` | Defines ignored, critical, and module path mappings |
| Analysis engine | `.github/scripts/test-impact.py` | Python script that evaluates changed files against the configuration |
| Reusable workflow | `.github/workflows/test-impact-analysis.yml` | GitHub Actions reusable workflow that orchestrates the analysis |
| E2E consumer | `.github/workflows/ui-e2e-tests-v2.yml` | Consumes TIA outputs to run targeted Playwright tests |
### Flow Diagram
```
PR opened/updated
|
v
+-------------------------------+
| tj-actions/changed-files | Gets list of changed files from PR
+-------------------------------+
|
v
+-------------------------------+
| test-impact.py |
| |
| 1. Filter ignored paths | docs/**, *.md, .gitignore, etc.
| 2. Check critical paths | prowler/lib/**, ui/lib/**, .github/workflows/**
| 3. Match modules | Map remaining files to module definitions
| 4. Categorize tests | Split into sdk-tests, api-tests, ui-e2e
+-------------------------------+
|
v
+-------------------------------+
| GitHub Actions Outputs |
| |
| run-all: true/false |
| sdk-tests: "tests/providers/aws/**"
| api-tests: "api/src/backend/api/tests/**"
| ui-e2e: "ui/tests/providers/**"
| modules: "sdk-aws,ui-providers"
| has-tests: true/false |
| has-sdk-tests: true/false |
| has-api-tests: true/false |
| has-ui-e2e: true/false |
+-------------------------------+
|
v
+-------------------------------+
| Consumer Workflows |
| |
| ui-e2e-tests-v2.yml: |
| - Path resolution pipeline |
| - Playwright execution |
+-------------------------------+
```
## Configuration Reference
The configuration lives in `.github/test-impact.yml` and contains three sections.
### `ignored` — Paths That Never Trigger Tests
Files matching these patterns are filtered out before any analysis takes place. This section is intended for non-code files.
```yaml
ignored:
paths:
- docs/**
- "*.md"
- .gitignore
- skills/**
- ui/tests/setups/** # E2E auth setup helpers (not runnable tests)
```
### `critical` — Paths That Trigger All Tests
If any changed file matches a critical path, the system short-circuits and outputs `run-all: true`. All downstream consumers then run their complete test suites.
```yaml
critical:
paths:
- prowler/lib/** # SDK core
- ui/lib/** # UI shared utilities
- ui/playwright.config.ts # Test infrastructure
- .github/workflows/** # CI changes
- .github/test-impact.yml # This config itself
```
### `modules` — Path-to-Test Mappings
Each module maps source file patterns to the tests that cover them.
```yaml
- name: ui-providers # Unique identifier
match: # Source file glob patterns
- ui/components/providers/**
- ui/actions/providers/**
- ui/app/**/providers/**
- ui/tests/providers/** # Test file changes also trigger themselves
tests: [] # SDK/API unit test patterns (empty for UI modules)
e2e: # Playwright E2E test patterns
- ui/tests/providers/**
```
#### Module Schema
| Field | Type | Description |
|-------|------|-------------|
| `name` | `string` | Unique module identifier (for example, `sdk-aws`, `ui-providers`, `api-views`) |
| `match` | `list[glob]` | Source file patterns that trigger this module |
| `tests` | `list[glob]` | Prowler SDK (`tests/`) or API (`api/`) unit test patterns to run |
| `e2e` | `list[glob]` | UI E2E test patterns (`ui/tests/`) to run |
#### Module Categories
- **`sdk-*`:** Provider and lib modules. These only produce `tests` output, not `e2e`.
- **`api-*`:** API views, serializers, filters, and role-based access control (RBAC). These produce `tests` and sometimes `e2e` (API changes can affect UI flows).
- **`ui-*`:** UI feature modules. These only produce `e2e` output, not `tests`.
## Path Resolution Pipeline
The E2E consumer workflow (`.github/workflows/ui-e2e-tests-v2.yml`, lines 202253) transforms the `ui-e2e` output from glob patterns into paths that Playwright can execute. This transformation follows a multi-step shell pipeline.
### Step 1: Check Run Mode
```bash
if [[ "${RUN_ALL_TESTS}" == "true" ]]; then
pnpm run test:e2e # Run everything, skip pipeline
fi
```
### Step 2: Strip the `ui/` Prefix and `**` Suffix
```bash
# "ui/tests/providers/**" -> "tests/providers/"
TEST_PATHS=$(echo "$E2E_TEST_PATHS" | sed 's|ui/||g' | sed 's|\*\*||g')
```
### Step 3: Filter Out Setup Paths
```bash
# Remove auth setup helpers (not runnable test suites)
TEST_PATHS=$(echo "$TEST_PATHS" | grep -v '^tests/setups/')
```
### Step 4: Safety Net for Bare `tests/`
If the pattern `ui/tests/**` was present in the output (from a critical path or a broad module like `ui-shadcn`), it resolves to bare `tests/` after stripping. This would cause Playwright to discover setup files in `tests/setups/`, so it gets expanded instead:
```bash
if echo "$TEST_PATHS" | grep -qx 'tests/'; then
# Expand to specific subdirs, excluding tests/setups/
for dir in tests/*/; do
[[ "$dir" == "tests/setups/" ]] && continue
SPECIFIC_DIRS="${SPECIFIC_DIRS}${dir}"
done
fi
```
### Step 5: Empty Directory Check
Directories that do not contain any `.spec.ts` or `.test.ts` files are skipped. This handles forward-looking patterns where a module is configured but tests have not been written yet.
```bash
if find "$p" -name '*.spec.ts' -o -name '*.test.ts' | head -1 | grep -q .; then
VALID_PATHS="${VALID_PATHS}${p}"
else
echo "Skipping empty test directory: $p"
fi
```
### Step 6: Execute Playwright
```bash
pnpm exec playwright test $TEST_PATHS
# For example: pnpm exec playwright test tests/providers/ tests/scans/
```
## Playwright Project Mapping
Playwright discovers tests by scanning the directories passed to it. The `playwright.config.ts` file defines projects with `testMatch` patterns that control which spec files each project claims:
```
tests/providers/providers.spec.ts -> "providers" project -> depends on admin.auth.setup
tests/scans/scans.spec.ts -> "scans" project -> depends on admin.auth.setup
tests/sign-in-base/*.spec.ts -> "sign-in-base" -> no auth dependency
tests/auth/*.spec.ts -> "auth" -> no auth dependency
tests/sign-up/sign-up.spec.ts -> "sign-up" -> no auth dependency
tests/invitations/invitations.spec.ts -> "invitations" -> depends on admin.auth.setup
```
Auth setup projects (`admin.auth.setup`, `manage-scans.auth.setup`, and others) create authenticated browser state files. Projects that declare them as `dependencies` wait for the setup to complete before running.
When TIA runs only `tests/providers/`, Playwright still automatically runs `admin.auth.setup` because the `providers` project declares it as a dependency.
## Edge Cases and Known Considerations
### Forward-Looking Patterns (Empty Test Directories)
A module can reference `ui/tests/attack-paths/**` before any tests exist there. The empty directory check (step 5) gracefully skips it instead of failing.
### Broad Patterns and the Safety Net
Modules like `ui-shadcn` and `api-views` list every E2E test suite explicitly to avoid using `ui/tests/**`. If a broad pattern does produce bare `tests/`, the safety net expands it to specific subdirectories, excluding `tests/setups/`.
### Setup Files and Auth Dependencies
`ui/tests/setups/**` is listed in the `ignored` section and also filtered in the path resolution pipeline. This double protection ensures setup files are never passed as test targets to Playwright. Auth setups run only when declared as project dependencies.
### Critical Path Triggering Run-All
Changes to `.github/workflows/**` or `.github/test-impact.yml` trigger `run-all: true`. This means editing any workflow file (even unrelated ones) runs the full test suite. This behavior is intentional — CI infrastructure changes should be validated broadly.
### Unmatched Files
Files that do not match any ignored, critical, or module pattern produce no test output. The `has-tests` flag is set to `false` and consumer workflows skip entirely via the `skip-e2e` job.
## Adding New Test Modules
To add tests for a new UI feature (for example, `dashboards`):
1. **Add the module to `.github/test-impact.yml`:**
```yaml
- name: ui-dashboards
match:
- ui/components/dashboards/**
- ui/actions/dashboards/**
- ui/app/**/dashboards/**
- ui/tests/dashboards/**
tests: []
e2e:
- ui/tests/dashboards/**
```
2. **Create the test directory and spec file:**
```
ui/tests/dashboards/dashboards.spec.ts
```
3. **Add a Playwright project in `ui/playwright.config.ts`:**
```typescript
{
name: "dashboards",
testMatch: "dashboards.spec.ts",
dependencies: ["admin.auth.setup"], // if tests need auth
},
```
4. **Register E2E paths in shared UI modules (if applicable):**
If the feature uses shared UI components, add the E2E path to the `ui-shadcn` module so that changes to shared components also trigger dashboard tests:
```yaml
- name: ui-shadcn
match:
- ui/components/shadcn/**
- ui/components/ui/**
e2e:
- ui/tests/dashboards/** # Add here
# ... existing paths
```
5. **Register E2E paths in API modules (if applicable):**
If API changes affect this feature, add the E2E path to the relevant `api-*` module (for example, `api-views`).
## Troubleshooting
### Tests Not Running When Expected
1. Check whether the changed file matches an `ignored` pattern. The script logs `[IGNORED]` to stderr.
2. Verify the file matches a module's `match` pattern. To test locally, run:
```bash
python .github/scripts/test-impact.py path/to/changed/file.ts
```
3. Confirm the module has non-empty `e2e` (for E2E) or `tests` (for unit tests).
4. Check the `has-ui-e2e` output — the consumer workflow gates on this flag.
### Unexpected Auth Setup Errors
Auth setup projects run automatically when a test project declares them as `dependencies`. If auth failures occur:
- **Verify secrets:** Confirm that the `E2E_ADMIN_USER` and `E2E_ADMIN_PASSWORD` secrets are set.
- **Check setup file existence:** Ensure the auth setup file exists in `ui/tests/setups/`.
- **Validate test match patterns:** Ensure the `testMatch` pattern in `playwright.config.ts` correctly matches the setup file.
### "No Tests Found" Errors
This typically means the path resolution pipeline produced valid directories but Playwright could not match any spec files to a project:
- **Check project configuration:** Verify that `playwright.config.ts` has a project with a `testMatch` pattern for the spec files in that directory.
- **Verify file naming:** Confirm the spec file naming matches the expected pattern (for example, `feature.spec.ts`).
### "No Runnable E2E Test Paths After Filtering Setups"
All resolved paths were under `tests/setups/`. This indicates the module's `e2e` patterns only point to setup files, which is a configuration error. The module should be updated to point to actual test directories.
### Debugging Locally
```bash
# See what the analysis engine produces for specific files
python .github/scripts/test-impact.py ui/components/providers/some-file.tsx
# Output goes to stderr (analysis log) and GITHUB_OUTPUT (structured output)
# Without the GITHUB_OUTPUT env var, results print to stderr only
```
+1 -2
View File
@@ -99,7 +99,7 @@
},
"user-guide/tutorials/prowler-app-rbac",
"user-guide/tutorials/prowler-app-api-keys",
"user-guide/tutorials/prowler-app-import-findings",
"user-guide/tutorials/prowler-app-findings-ingestion",
{
"group": "Mutelist",
"expanded": true,
@@ -131,7 +131,6 @@
"user-guide/tutorials/prowler-app-lighthouse-multi-llm"
]
},
"user-guide/tutorials/prowler-app-attack-paths",
"user-guide/tutorials/prowler-cloud-public-ips",
{
"group": "Tutorials",
@@ -10,7 +10,7 @@ Complete reference guide for all tools available in the Prowler MCP Server. Tool
|----------|------------|------------------------|
| Prowler Hub | 10 tools | No |
| Prowler Documentation | 2 tools | No |
| Prowler Cloud/App | 27 tools | Yes |
| Prowler Cloud/App | 24 tools | Yes |
## Tool Naming Convention
@@ -80,14 +80,6 @@ Tools for managing finding muting, including pattern-based bulk muting (mutelist
- **`prowler_app_update_mute_rule`** - Update a mute rule's name, reason, or enabled status
- **`prowler_app_delete_mute_rule`** - Delete a mute rule from the system
### Attack Paths Analysis
Tools for analyzing privilege escalation chains and security misconfigurations using graph-based analysis. Attack Paths maps relationships between cloud resources, permissions, and security findings to detect how privileges can be escalated and how misconfigurations can be exploited.
- **`prowler_app_list_attack_paths_scans`** - List Attack Paths scans with filtering by provider, provider type, and scan state (available, scheduled, executing, completed, failed, cancelled)
- **`prowler_app_list_attack_paths_queries`** - Discover available Attack Paths queries for a completed scan, including query names, descriptions, and required parameters
- **`prowler_app_run_attack_paths_query`** - Execute an Attack Paths query against a completed scan and retrieve graph results with nodes (cloud resources, findings, virtual nodes) and relationships (access paths, role assumptions, security group memberships)
### Compliance Management
Tools for viewing compliance status and framework details across all cloud providers.
@@ -121,8 +121,8 @@ To update the environment file:
Edit the `.env` file and change version values:
```env
PROWLER_UI_VERSION="5.20.0"
PROWLER_API_VERSION="5.20.0"
PROWLER_UI_VERSION="5.18.0"
PROWLER_API_VERSION="5.18.0"
```
<Note>
@@ -24,7 +24,6 @@ Full access to Prowler Cloud platform and self-managed Prowler App for:
- **Scan Orchestration**: Trigger on-demand scans and schedule recurring security assessments
- **Resource Inventory**: Search and view detailed information about your audited resources
- **Muting Management**: Create and manage muting lists/rules to suppress non-relevant findings
- **Attack Paths Analysis**: Analyze privilege escalation chains and security misconfigurations through graph-based analysis of cloud resource relationships
### 2. Prowler Hub
@@ -62,7 +61,6 @@ The Prowler MCP Server enables powerful workflows through AI assistants:
- "Show me all critical findings from my AWS production accounts"
- "Register my new AWS account in Prowler and run a scheduled scan every day"
- "List all muted findings and detect what findgings are muted by a not enough good reason in relation to their severity"
- "Run an attack paths query to find EC2 instances exposed to the Internet with access to sensitive S3 buckets"
**Security Research**
- "Explain what the S3 bucket public access Prowler check does"
@@ -14,8 +14,8 @@
<text x="140" y="107" text-anchor="middle" font-size="16" font-weight="700" fill="#fff">1</text>
<text x="140" y="145" text-anchor="middle" font-size="15" font-weight="700" fill="#1a1a2e">Create Management</text>
<text x="140" y="165" text-anchor="middle" font-size="15" font-weight="700" fill="#1a1a2e">Account Role</text>
<rect x="60" y="185" width="160" height="24" rx="12" fill="#E8F0FE"/>
<text x="140" y="201" text-anchor="middle" font-size="11" font-weight="600" fill="#4285F4">Quick Create or Manual</text>
<rect x="80" y="185" width="120" height="24" rx="12" fill="#E8F0FE"/>
<text x="140" y="201" text-anchor="middle" font-size="11" font-weight="600" fill="#4285F4">Manually in IAM</text>
<text x="140" y="232" text-anchor="middle" font-size="12" fill="#5f6368">Allows Prowler to</text>
<text x="140" y="248" text-anchor="middle" font-size="12" fill="#5f6368">discover your org</text>
<text x="140" y="264" text-anchor="middle" font-size="12" fill="#5f6368">structure</text>

Before

Width:  |  Height:  |  Size: 4.7 KiB

After

Width:  |  Height:  |  Size: 4.7 KiB

@@ -46,8 +46,8 @@
<text x="82" y="384" font-size="11" fill="#4285F4" font-weight="600">organizations:ListTagsForResource</text>
<!-- Deploy badge -->
<rect x="115" y="400" width="270" height="28" rx="14" fill="#FFF3E0" stroke="#F9AB00" stroke-width="1.5"/>
<text x="250" y="419" text-anchor="middle" font-size="12" font-weight="700" fill="#E65100">Deploy: Quick Create link or Manual</text>
<rect x="145" y="400" width="210" height="28" rx="14" fill="#FFF3E0" stroke="#F9AB00" stroke-width="1.5"/>
<text x="250" y="419" text-anchor="middle" font-size="12" font-weight="700" fill="#E65100">Deploy: MANUALLY in IAM Console</text>
<!-- ===== Prowler Cloud connector ===== -->
<rect x="490" y="195" width="120" height="36" rx="8" fill="#F5F5F5" stroke="#E0E0E0" stroke-width="1"/>

Before

Width:  |  Height:  |  Size: 6.0 KiB

After

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 554 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 326 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 760 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 665 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 568 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 747 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 289 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 326 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 589 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 115 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 316 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 304 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 477 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 575 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 324 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 115 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 423 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 275 KiB

+3 -4
View File
@@ -33,13 +33,12 @@ The supported providers right now are:
| [Github](/user-guide/providers/github/getting-started-github) | Official | Organizations / Repositories | UI, API, CLI |
| [Oracle Cloud](/user-guide/providers/oci/getting-started-oci) | Official | Tenancies / Compartments | UI, API, CLI |
| [Alibaba Cloud](/user-guide/providers/alibabacloud/getting-started-alibabacloud) | Official | Accounts | UI, API, CLI |
| [Cloudflare](/user-guide/providers/cloudflare/getting-started-cloudflare) | Official | Accounts | UI, API, CLI |
| [Cloudflare](/user-guide/providers/cloudflare/getting-started-cloudflare) | Official | Accounts | CLI |
| [Infra as Code](/user-guide/providers/iac/getting-started-iac) | Official | Repositories | UI, API, CLI |
| [MongoDB Atlas](/user-guide/providers/mongodbatlas/getting-started-mongodbatlas) | Official | Organizations | UI, API, CLI |
| [OpenStack](/user-guide/providers/openstack/getting-started-openstack) | Official | Projects | UI, API, CLI |
| [OpenStack](/user-guide/providers/openstack/getting-started-openstack) | Official | Projects | CLI |
| [LLM](/user-guide/providers/llm/getting-started-llm) | Official | Models | CLI |
| [Image](/user-guide/providers/image/getting-started-image) | Official | Container Images | CLI, API |
| [Google Workspace](/user-guide/providers/googleworkspace/getting-started-googleworkspace) | Official | Domains | CLI |
| [Image](/user-guide/providers/image/getting-started-image) | Official | Container Images | CLI |
| **NHN** | Unofficial | Tenants | CLI |
For more information about the checks and compliance of each provider visit [Prowler Hub](https://hub.prowler.com).
@@ -73,7 +73,6 @@ The following list includes all the AWS checks with configurable variables that
| `ssm_documents_set_as_public` | `trusted_account_ids` | List of Strings |
| `vpc_endpoint_connections_trust_boundaries` | `trusted_account_ids` | List of Strings |
| `vpc_endpoint_services_allowed_principals_trust_boundaries` | `trusted_account_ids` | List of Strings |
| `opensearch_service_domains_not_publicly_accessible` | `trusted_ips` | List of Strings |
## Azure
+1 -1
View File
@@ -99,7 +99,7 @@ def get_table(data):
## S3 Integration
If you are using Prowler Cloud with the S3 integration or that integration from Prowler CLI and you want to use your data from your S3 bucket, you can run the following command in order to load the dashboard with the new files:
If you are using Prowler SaaS with the S3 integration or that integration from Prowler Open Source and you want to use your data from your S3 bucket, you can run the following command in order to load the dashboard with the new files:
```sh
aws s3 cp s3://<your-bucket>/output/csv ./output --recursive
@@ -16,33 +16,23 @@ Prowler requires Alibaba Cloud credentials to perform security checks. Authentic
### Credentials URI (Recommended for Centralized Services)
Prowler can retrieve credentials from an external URI endpoint. Provide the URI via the `--credentials-uri` flag or the `ALIBABA_CLOUD_CREDENTIALS_URI` environment variable. The URI must return credentials in the standard JSON format.
If `--credentials-uri` is provided (or `ALIBABA_CLOUD_CREDENTIALS_URI` environment variable), Prowler will retrieve credentials from the specified external URI endpoint. The URI must return credentials in the standard JSON format.
```bash
# Using CLI flag
prowler alibabacloud --credentials-uri http://localhost:8080/credentials
# Or using environment variable
export ALIBABA_CLOUD_CREDENTIALS_URI="http://localhost:8080/credentials"
prowler alibabacloud
```
### OIDC Role Authentication (Recommended for ACK/Kubernetes)
OIDC authentication assumes the specified role using an OIDC token. This is the most secure method for containerized applications running in ACK (Alibaba Container Service for Kubernetes) with RRSA enabled.
The role ARN can be provided via the `--oidc-role-arn` flag or the `ALIBABA_CLOUD_ROLE_ARN` environment variable. The OIDC provider ARN and token file must be set via environment variables:
If OIDC environment variables are set, Prowler will use OIDC authentication to assume the specified role. This is the most secure method for containerized applications running in ACK (Alibaba Container Service for Kubernetes) with RRSA enabled.
Required environment variables:
- `ALIBABA_CLOUD_ROLE_ARN`
- `ALIBABA_CLOUD_OIDC_PROVIDER_ARN`
- `ALIBABA_CLOUD_OIDC_TOKEN_FILE`
```bash
# Using CLI flag for role ARN
export ALIBABA_CLOUD_OIDC_PROVIDER_ARN="acs:ram::123456789012:oidc-provider/ack-rrsa-provider"
export ALIBABA_CLOUD_OIDC_TOKEN_FILE="/var/run/secrets/tokens/oidc-token"
prowler alibabacloud --oidc-role-arn acs:ram::123456789012:role/YourRole
# Or using all environment variables
export ALIBABA_CLOUD_ROLE_ARN="acs:ram::123456789012:role/YourRole"
export ALIBABA_CLOUD_OIDC_PROVIDER_ARN="acs:ram::123456789012:oidc-provider/ack-rrsa-provider"
export ALIBABA_CLOUD_OIDC_TOKEN_FILE="/var/run/secrets/tokens/oidc-token"
@@ -64,17 +54,9 @@ prowler alibabacloud
### RAM Role Assumption (Recommended for Cross-Account)
For cross-account access, use RAM role assumption. Provide the initial credentials (access keys) via environment variables and the target role ARN via the `--role-arn` flag or the `ALIBABA_CLOUD_ROLE_ARN` environment variable.
The `--role-session-name` flag customizes the session identifier (defaults to `ProwlerAssessmentSession`).
For cross-account access, use RAM role assumption. You must provide the initial credentials (access keys) and the target role ARN.
```bash
# Using CLI flags
export ALIBABA_CLOUD_ACCESS_KEY_ID="your-access-key-id"
export ALIBABA_CLOUD_ACCESS_KEY_SECRET="your-access-key-secret"
prowler alibabacloud --role-arn acs:ram::123456789012:role/ProwlerAuditRole --role-session-name MyAuditSession
# Or using all environment variables
export ALIBABA_CLOUD_ACCESS_KEY_ID="your-access-key-id"
export ALIBABA_CLOUD_ACCESS_KEY_SECRET="your-access-key-secret"
export ALIBABA_CLOUD_ROLE_ARN="acs:ram::123456789012:role/ProwlerAuditRole"
@@ -117,12 +117,6 @@ prowler alibabacloud
#### RAM Role Assumption
```bash
# Using --role-arn CLI flag
export ALIBABA_CLOUD_ACCESS_KEY_ID="your-access-key-id"
export ALIBABA_CLOUD_ACCESS_KEY_SECRET="your-access-key-secret"
prowler alibabacloud --role-arn acs:ram::123456789012:role/ProwlerAuditRole
# Or using environment variables
export ALIBABA_CLOUD_ACCESS_KEY_ID="your-access-key-id"
export ALIBABA_CLOUD_ACCESS_KEY_SECRET="your-access-key-secret"
export ALIBABA_CLOUD_ROLE_ARN="acs:ram::123456789012:role/ProwlerAuditRole"
@@ -1,130 +1,119 @@
---
title: "Cloudflare Authentication in Prowler"
title: 'Cloudflare Authentication in Prowler'
---
import { VersionBadge } from "/snippets/version-badge.mdx"
Prowler for Cloudflare supports the following authentication methods:
<VersionBadge version="5.17.0" />
Prowler for Cloudflare supports two authentication methods, both available in Prowler Cloud and Prowler CLI:
- [**API Token**](#api-token-recommended) (**Recommended**) — Scoped, least-privilege access to specific permissions and zones.
- [**API Key and Email**](#api-key-and-email-legacy) (**Legacy**) — Global access to the entire account using the Global API Key.
<Warning>
**Use only one authentication method at a time.** If both API Token and API Key + Email are set, Prowler uses the API Token and logs an error about the conflict.
</Warning>
- [**API Token**](#api-token-recommended) (**Recommended**)
- [**API Key and Email (Legacy)**](#api-key-and-email-legacy)
## Required Permissions
Prowler requires read-only access to Cloudflare zones and their settings. The following permissions must be configured when creating the API Token:
Prowler requires read-only access to your Cloudflare zones and their settings. The following permissions are needed:
| Resource | Permission | Access | Description |
|----------|------------|--------|-------------|
| `Account` | `Account Settings` | `Read` | Required to list accounts and verify user identity |
| `Zone` | `Zone` | `Read` | Required to list zones, rulesets, bot management, and SSL settings |
| `Zone` | `Zone Settings` | `Read` | Required to read zone security settings (TLS, HSTS, WAF, etc.) |
| `Zone` | `DNS` | `Read` | Required to read DNS records and DNSSEC status |
| Permission | Description |
|------------|-------------|
| `Zone:Read` | Read access to zone settings and configurations |
| `Zone Settings:Read` | Read access to zone security settings (SSL/TLS, HSTS, etc.) |
| `DNS:Read` | Read access to DNS records (for DNSSEC checks) |
<Warning>
Ensure the API Token has access to all zones targeted for scanning. Missing permissions may cause some checks to fail or return incomplete results.
Ensure your API Token or API Key has access to all zones you want to scan. If permissions are missing, some checks may fail or return incomplete results.
</Warning>
---
## API Token (Recommended)
User API Tokens are the recommended authentication method because they:
API Tokens are the recommended authentication method because they:
- Can be scoped to specific permissions and zones
- Are more secure than global API keys
- Can be easily rotated without affecting other integrations
<Note>
Create a **User API Token**, not an Account API Token. User API Tokens are created from the profile settings and offer finer permission control.
</Note>
### Step 1: Create an API Token
### Step 1: Create a User API Token
1. **Log into Cloudflare Dashboard**
- Go to [https://dash.cloudflare.com](https://dash.cloudflare.com) and sign in
1. Log into the [Cloudflare Dashboard](https://dash.cloudflare.com).
2. Click on the profile icon in the top right corner, then select "My Profile".
3. Click on the **API Tokens** tab.
4. Click **Create Token**, then select **Create Custom Token** at the bottom of the page.
5. Configure the token with the following settings:
- **Token name:** A descriptive name (e.g., "Prowler Security Scanner")
- **Permissions:**
- `Account` — `Account Settings` — `Read`
- `Zone` — `Zone` — `Read`
- `Zone` — `Zone Settings` — `Read`
- `Zone` — `DNS` — `Read`
- **Zone Resources:** Select either:
- **Include → All zones** (to scan all zones in the account)
2. **Navigate to API Tokens**
- Click on your profile icon in the top right corner
- Select **My Profile**
- Click on the **API Tokens** tab
3. **Create a Custom Token**
- Click **Create Token**
- Select **Create Custom Token** (at the bottom)
4. **Configure Token Permissions**
Give your token a descriptive name (e.g., "Prowler Security Scanner") and add the [required permissions](#required-permissions) listed above.
5. **Set Zone Resources**
- Under **Zone Resources**, select either:
- **Include → All zones** (to scan all zones in your account)
- **Include → Specific zone** (to limit access to specific zones)
![Token Permissions](/images/providers/cloudflare-token-permissions.png)
6. **Create and Copy Token**
- Click **Continue to summary**
- Review the permissions and click **Create Token**
- **Copy the token immediately** - Cloudflare will only show it once
6. Configure the **Account Resources** and **Zone Resources**, and optionally set a **TTL** for the token expiration. Click **Continue to summary**.
### Step 2: Store the Token Securely
![Token Resources and TTL](/images/providers/cloudflare-token-save.png)
Store your API token as an environment variable:
7. Review the permissions and click **Create Token**.
8. Copy the token immediately.
<Warning>
Cloudflare only displays the token once. Copy it immediately and store it securely. If lost, a new token must be created.
</Warning>
### Step 2: Provide the Token to Prowler
- **Prowler Cloud:** Paste the token in the credentials form when configuring the Cloudflare provider.
- **Prowler CLI:** Export the token as an environment variable:
```console
```bash
export CLOUDFLARE_API_TOKEN="your-api-token-here"
prowler cloudflare
```
---
<Warning>
Never commit API tokens to version control or share them in plain text. Use environment variables or a secrets manager.
</Warning>
## API Key and Email (Legacy)
API Keys provide full access to the Cloudflare account. While supported, this method is less secure than API Tokens because it grants broader permissions.
API Keys provide full access to your Cloudflare account. While supported, this method is less secure than API Tokens because it grants broader permissions.
### Step 1: Get the Global API Key
### Step 1: Get Your API Key
1. Log into the [Cloudflare Dashboard](https://dash.cloudflare.com).
2. Click on the profile icon in the top right corner, then select "My Profile".
3. Click on the **API Tokens** tab.
4. Scroll down to the **API Keys** section.
5. Click **View** next to **Global API Key**.
6. Enter the account password to reveal the key, then copy it.
1. **Log into Cloudflare Dashboard**
- Go to [https://dash.cloudflare.com](https://dash.cloudflare.com) and sign in
### Step 2: Provide the Credentials to Prowler
2. **Navigate to API Tokens**
- Click on your profile icon in the top right corner
- Select **My Profile**
- Click on the **API Tokens** tab
- **Prowler Cloud:** Enter the Global API Key and email in the credentials form when configuring the Cloudflare provider.
- **Prowler CLI:** Export both values as environment variables:
3. **View Global API Key**
- Scroll down to the **API Keys** section
- Click **View** next to **Global API Key**
- Enter your password to reveal the key
- Copy the API key
```console
### Step 2: Store Credentials Securely
Store both your API key and email as environment variables:
```bash
export CLOUDFLARE_API_KEY="your-api-key-here"
export CLOUDFLARE_API_EMAIL="your-email@example.com"
prowler cloudflare
```
<Note>
The email must match the email address used to log into the Cloudflare account.
The email must be the same email address used to log into your Cloudflare account.
</Note>
---
## Best Practices
- **Use API Tokens instead of API Keys** — Tokens can be scoped to specific permissions and zones.
- **Use environment variables** — Never hardcode credentials in scripts or commands.
- **Rotate credentials regularly** — Create new tokens periodically and revoke old ones.
- **Use least privilege** — Only grant the minimum permissions needed for scanning.
- **Monitor token usage** — Review the Cloudflare audit log for suspicious activity.
### Security Recommendations
---
- **Use API Tokens instead of API Keys** - Tokens can be scoped to specific permissions
- **Use environment variables** - Never hardcode credentials in scripts or commands
- **Rotate credentials regularly** - Create new tokens periodically and revoke old ones
- **Use least privilege** - Only grant the minimum permissions needed
- **Monitor token usage** - Review the Cloudflare audit log for suspicious activity
<Warning>
**Use only one authentication method at a time.** If both API Token and API Key + Email are set, Prowler will use the API Token and log an error message.
</Warning>
## Troubleshooting
@@ -134,15 +123,20 @@ This error occurs when using API Key authentication without providing the email
### "Authentication error" or "Permission denied"
- Verify the API Token or API Key is correct and not expired.
- Check that the token has the [required permissions](#required-permissions).
- Ensure the token has access to the zones targeted for scanning.
- Verify your API Token or API Key is correct and not expired
- Check that your token has the [required permissions](#required-permissions)
- Ensure your token has access to the zones you're trying to scan
### "Both API Token and API Key and Email credentials are set"
This warning appears when all three environment variables are set (`CLOUDFLARE_API_TOKEN`, `CLOUDFLARE_API_KEY`, `CLOUDFLARE_API_EMAIL`). To resolve, unset the credentials that are not needed:
This warning appears when all three environment variables are set:
- `CLOUDFLARE_API_TOKEN`
- `CLOUDFLARE_API_KEY`
- `CLOUDFLARE_API_EMAIL`
```console
To resolve, unset the credentials you don't want to use:
```bash
# To use API Token only (recommended)
unset CLOUDFLARE_API_KEY
unset CLOUDFLARE_API_EMAIL
@@ -150,7 +144,3 @@ unset CLOUDFLARE_API_EMAIL
# Or to use API Key and Email only
unset CLOUDFLARE_API_TOKEN
```
### "Account not found" Error
This error occurs when a specified `--account-id` is not accessible with the current credentials. Verify the Account ID is correct and that the credentials have access to the target account.
@@ -1,165 +1,117 @@
---
title: 'Getting Started With Cloudflare on Prowler'
title: 'Getting Started with Cloudflare'
---
import { VersionBadge } from "/snippets/version-badge.mdx"
Prowler for Cloudflare scans zones for security misconfigurations, including SSL/TLS settings, DNSSEC, HSTS, WAF rules, DNS records, and more.
## Prerequisites
Set up authentication for Cloudflare with the [Cloudflare Authentication](/user-guide/providers/cloudflare/authentication) guide before starting either path:
- Create a Cloudflare User API Token (recommended) or locate the Global API Key
- Grant the required read-only permissions (`Account Settings:Read`, `Zone:Read`, `Zone Settings:Read`, `DNS:Read`)
- Identify the Cloudflare Account ID to use as the provider identifier
<CardGroup cols={2}>
<Card title="Prowler Cloud" icon="cloud" href="#prowler-cloud">
Onboard Cloudflare using Prowler Cloud
</Card>
<Card title="Prowler CLI" icon="terminal" href="#prowler-cli">
Onboard Cloudflare using Prowler CLI
</Card>
</CardGroup>
## Prowler Cloud
<VersionBadge version="5.19.0" />
### Step 1: Locate the Account ID
1. Log into the [Cloudflare Dashboard](https://dash.cloudflare.com).
2. Select any zone in the target account.
3. On the zone overview page, find the **Account ID** in the right sidebar under the "API" section.
![Cloudflare Account ID](/images/providers/cloudflare-account-id.png)
<Note>
The Account ID is a 32-character hexadecimal string (e.g., `372e67954025e0ba6aaa6d586b9e0b59`). This value acts as the unique identifier for the Cloudflare account in Prowler Cloud.
</Note>
### Step 2: Open Prowler Cloud
1. Go to [Prowler Cloud](https://cloud.prowler.com/) or launch [Prowler App](/user-guide/tutorials/prowler-app).
2. Navigate to "Configuration" > "Cloud Providers".
![Cloud Providers Page](/images/prowler-app/cloud-providers-page.png)
3. Click "Add Cloud Provider".
![Add a Cloud Provider](/images/prowler-app/add-cloud-provider.png)
4. Select "Cloudflare".
![Select Cloudflare](/images/providers/select-cloudflare-prowler-cloud.png)
5. Add the **Account ID** and an optional alias, then click "Next".
![Add Cloudflare Account ID](/images/providers/cloudflare-account-id-form.png)
### Step 3: Choose and Provide Authentication
After the Account ID is in place, select the authentication method that matches the Cloudflare setup:
![Select Authentication Method](/images/providers/cloudflare-auth-selection.png)
#### User API Token Authentication (Recommended)
1. Select **API Token**.
2. Enter the **User API Token** created in the Cloudflare Dashboard.
![API Token Form](/images/providers/cloudflare-token-form.png)
Use this method for scoped, least-privilege access. Full setup steps are in the [Authentication guide](/user-guide/providers/cloudflare/authentication#api-token-recommended).
#### API Key and Email Authentication (Legacy)
1. Select **API Key + Email**.
2. Enter the **Global API Key**.
3. Enter the **email address** associated with the Cloudflare account.
![API Key and Email Form](/images/providers/cloudflare-api-email-form.png)
For the complete setup workflow, follow the [Authentication guide](/user-guide/providers/cloudflare/authentication#api-key-and-email-legacy).
### Step 4: Launch the Scan
1. Review the summary.
2. Click **Launch Scan** to start auditing Cloudflare.
![Launch Scan](/images/providers/cloudflare-launch-scan.png)
---
## Prowler CLI
import { VersionBadge } from "/snippets/version-badge.mdx";
<VersionBadge version="5.17.0" />
Prowler for Cloudflare allows you to scan your Cloudflare zones for security misconfigurations, including SSL/TLS settings, DNSSEC, HSTS, and more.
## Prerequisites
Before running Prowler with the Cloudflare provider, ensure you have:
1. A Cloudflare account with at least one zone
2. One of the following authentication methods configured (see [Authentication](/user-guide/providers/cloudflare/authentication)):
- An **API Token** (recommended)
- An **API Key + Email** (legacy)
## Quick Start
### Step 1: Set Up Authentication
Choose the matching method from the [Cloudflare Authentication](/user-guide/providers/cloudflare/authentication) guide:
The recommended method is using an API Token via environment variable:
- **User API Token** (recommended): Set `CLOUDFLARE_API_TOKEN`
- **API Key + Email** (legacy): Set `CLOUDFLARE_API_KEY` and `CLOUDFLARE_API_EMAIL`
```bash
export CLOUDFLARE_API_TOKEN="your-api-token-here"
```
### Step 2: Run the First Scan
Alternatively, use API Key + Email:
Run a baseline scan after credentials are configured:
```bash
export CLOUDFLARE_API_KEY="your-api-key-here"
export CLOUDFLARE_API_EMAIL="your-email@example.com"
```
```console
### Step 2: Run Prowler
Run a scan across all your Cloudflare zones:
```bash
prowler cloudflare
```
Prowler automatically discovers all zones accessible with the provided credentials and runs security checks against them.
That's it! Prowler will automatically discover all zones in your account and run security checks against them.
### Step 3: Filter the Scan Scope (Optional)
## Authentication
#### Filter by Zone
Prowler reads Cloudflare credentials from environment variables. Set your credentials before running Prowler:
**API Token (Recommended):**
```bash
export CLOUDFLARE_API_TOKEN="your-api-token-here"
prowler cloudflare
```
**API Key + Email (Legacy):**
```bash
export CLOUDFLARE_API_KEY="your-api-key-here"
export CLOUDFLARE_API_EMAIL="your-email@example.com"
prowler cloudflare
```
## Filtering Zones
By default, Prowler scans all zones accessible with your credentials:
```bash
prowler cloudflare
```
To scan only specific zones, use the `-f`, `--region`, or `--filter-region` argument:
```console
```bash
prowler cloudflare -f example.com
```
Multiple zones can be specified:
You can specify multiple zones:
```console
```bash
prowler cloudflare -f example.com example.org
```
Zone IDs are also supported:
You can also use zone IDs instead of domain names:
```console
```bash
prowler cloudflare -f 023e105f4ecef8ad9ca31a8372d0c353
```
#### Filter by Account
## Filtering Accounts
To restrict the scan to specific accounts, use the `--account-id` argument:
By default, Prowler scans all accounts accessible with your credentials. If your API Token or API Key has access to multiple Cloudflare accounts, you can restrict the scan to specific accounts using the `--account-id` argument:
```console
```bash
prowler cloudflare --account-id 372e67954025e0ba6aaa6d586b9e0b59
```
Multiple account IDs can be specified:
You can specify multiple account IDs:
```console
```bash
prowler cloudflare --account-id 372e67954025e0ba6aaa6d586b9e0b59 9a7806061c88ada191ed06f989cc3dac
```
<Note>
If any of the provided account IDs are not accessible with the current credentials, Prowler raises an error and stops execution.
If any of the provided account IDs are not found among the accounts accessible with your credentials, Prowler will raise an error and stop execution.
</Note>
Account and zone filtering can be combined to narrow the scan scope further:
You can combine account and zone filtering to narrow the scan scope further:
```console
```bash
prowler cloudflare --account-id 372e67954025e0ba6aaa6d586b9e0b59 -f example.com
```
### Step 4: Use a Custom Configuration (Optional)
## Configuration
Prowler uses a configuration file to customize provider behavior. The Cloudflare configuration includes:
@@ -171,8 +123,10 @@ cloudflare:
To use a custom configuration:
```console
```bash
prowler cloudflare --config-file /path/to/config.yaml
```
---
## Next Steps
- [Authentication](/user-guide/providers/cloudflare/authentication) - Detailed guide on creating API tokens and keys
@@ -135,16 +135,3 @@ prowler gcp --impersonate-service-account <service-account-email>
```
More details on authentication methods in the [Authentication](/user-guide/providers/gcp/authentication) page.
### Skip API Check
By default, Prowler verifies which Google Cloud APIs are enabled before running checks for each service. To skip this verification and assume all APIs are active, use the `--skip-api-check` flag:
```console
prowler gcp --skip-api-check
```
<Note>
This is useful when the authenticated principal lacks the `serviceusage.services.list` permission but has access to individual service APIs.
</Note>
@@ -135,7 +135,7 @@ prowler iac --scan-path ./my-iac-directory --scanners vuln misconfig
#### Exclude Paths
```sh
prowler iac --scan-path ./my-iac-directory --exclude-path ./my-iac-directory/test ./my-iac-directory/examples
prowler iac --scan-path ./my-iac-directory --exclude-path ./my-iac-directory/test,./my-iac-directory/examples
```
### Output
@@ -127,18 +127,4 @@ Include PowerShell module initialization to run every check:
prowler m365 --sp-env-auth --init-modules
```
### Region Selection
By default, Prowler connects to the global Microsoft 365 environment (`M365Global`). To target a different cloud environment, use the `--region` flag:
```console
prowler m365 --sp-env-auth --region M365USGovernment
```
Available regions:
* **M365Global** (default): Standard commercial cloud
* **M365China**: China-operated cloud (21Vianet)
* **M365USGovernment**: US Government cloud (GCC High)
---
@@ -4,11 +4,13 @@ title: 'Getting Started With OpenStack'
import { VersionBadge } from "/snippets/version-badge.mdx"
Prowler supports OpenStack both from the CLI and from Prowler Cloud. This guide walks you through the requirements, how to connect the provider in the UI, and how to run scans from the command line.
<VersionBadge version="5.18.0" />
<Note>
Prowler for OpenStack allows you to audit your OpenStack cloud infrastructure for security misconfigurations, including compute instances, networking, identity and access management, storage, and more.
<Warning>
Prowler currently supports **public cloud OpenStack providers** (OVH, Infomaniak, Vexxhost, etc.). Support for self-deployed OpenStack environments is not yet available, if you are interested in this feature, please [open an issue](https://github.com/prowler-cloud/prowler/issues/new) or [contact us](https://prowler.com/contact).
</Note>
</Warning>
## Prerequisites
@@ -20,47 +22,16 @@ Before running Prowler with the OpenStack provider, ensure you have:
4. Access to Prowler CLI (see [Installation](/getting-started/installation/prowler-cli)) or an account created in [Prowler Cloud](https://cloud.prowler.com)
<CardGroup cols={2}>
<Card title="Prowler Cloud" icon="cloud" href="#prowler-cloud">
Onboard OpenStack using Prowler Cloud
</Card>
<Card title="Prowler CLI" icon="terminal" href="#prowler-cli">
Onboard OpenStack using Prowler CLI
Run OpenStack security audits with Prowler CLI
</Card>
<Card title="Authentication Methods" icon="key" href="/user-guide/providers/openstack/authentication">
Learn about OpenStack authentication options
</Card>
</CardGroup>
## Prowler Cloud
<VersionBadge version="5.19.0" />
### Step 1: Add the Provider
1. Navigate to "Cloud Providers" and click "Add Cloud Provider".
![Providers List](./images/select-provider.png)
2. Select "OpenStack" from the provider list.
3. Enter the "Project ID" from the OpenStack provider.
![Add project ID form](./images/add-provider-id.png)
4. (Optional) Add a friendly alias to identify this project in dashboards.
### Step 2: Provide Credentials
1. Click "Next" to open the credentials form.
2. Paste the full content of the `clouds.yaml` file into the "Clouds YAML Content" field. This file is available in the OpenStack provider's Horizon dashboard (see the [Authentication guide](/user-guide/providers/openstack/authentication) for detailed instructions).
3. Enter the "Cloud Name" — this is the key that identifies the cloud entry inside the `clouds.yaml` file (e.g., `mycloud`).
![Credentials form](./images/add-credentials.png)
### Step 3: Test the Connection and Start Scanning
1. Click "Test connection" to ensure Prowler Cloud can reach the OpenStack API.
![Test connection](./images/test-connection.png)
2. The provider will appear in the list with its current connection status.
3. Launch a scan from the provider row or from the "Scans" page.
---
## Prowler CLI
<VersionBadge version="5.18.0" />
### Step 1: Set Up Authentication
Download the `clouds.yaml` file from your OpenStack provider (see [Authentication guide](/user-guide/providers/openstack/authentication) for detailed instructions) and save it to `~/.config/openstack/clouds.yaml`:
@@ -127,7 +98,7 @@ Run a baseline scan of your OpenStack cloud:
prowler openstack --clouds-yaml-cloud openstack
```
Replace `openstack` with the custom cloud name defined in the `clouds.yaml` file (e.g., `ovh-production`).
Replace `openstack` with your cloud name if you customized it in the `clouds.yaml` file (e.g., `ovh-production`).
Prowler will automatically discover and audit all supported OpenStack services in your project.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 135 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 99 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 241 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 102 KiB

@@ -1,220 +0,0 @@
---
title: "Attack Paths"
description: "Identify privilege escalation chains and security misconfigurations across cloud environments using graph-based analysis."
---
import { VersionBadge } from "/snippets/version-badge.mdx"
<VersionBadge version="5.17.0" />
Attack Paths analyzes relationships between cloud resources, permissions, and security findings to detect how privileges can be escalated and how misconfigurations can be exploited by threat actors.
By mapping these relationships as a graph, Attack Paths reveals risks that individual security checks cannot detect on their own — such as an IAM role that can escalate its own permissions, or a chain of policies that grants unintended access to sensitive resources.
<Note>
Attack Paths is currently available for **AWS** providers. Support for
additional providers is planned.
</Note>
## Prerequisites
The following prerequisites are required for Attack Paths:
- **An AWS provider is configured** with valid credentials in Prowler App. For setup instructions, see [Getting Started with AWS](/user-guide/providers/aws/getting-started-aws).
- **At least one scan has completed** on the configured AWS provider. Attack Paths scans run automatically alongside regular security scans — no separate configuration is required.
## How Attack Paths Scans Work
Attack Paths scans are generated automatically when a security scan runs on an AWS provider. Each completed scan produces graph data that maps relationships between IAM principals, policies, trust configurations, and other resources.
Once the scan finishes and the graph data is ready, the scan appears in the Attack Paths scan table with a **Completed** status. Scans that are still processing display as **Executing** or **Scheduled**.
<Note>
Since Prowler scans all configured providers every **24 hours** by default,
Attack Paths data stays up to date automatically.
</Note>
## Accessing Attack Paths
To open Attack Paths, click **Attack Paths** in the left navigation menu.
<img
src="/images/prowler-app/attack-paths/navigation.png"
alt="Attack Paths navigation menu entry"
width="700"
/>
The main interface is divided into two areas:
- **Left panel:** A table listing all available Attack Paths scans
- **Right panel:** The query selector, parameter form, and execute controls
## Selecting a Scan
The scans table displays all Attack Paths scans with the following columns:
- **Provider / Account:** The AWS provider alias and account identifier
- **Last scan date:** When the scan completed
- **Status:** Current state of the scan (Completed, Executing, Scheduled, or Failed)
- **Progress:** Completion percentage for in-progress scans
- **Duration:** Total scan time
To select a scan for analysis, click **Select** on any row with a **Completed** status.
<img
src="/images/prowler-app/attack-paths/scan-list-table.png"
alt="Attack Paths scan list table showing completed scans"
width="700"
/>
<Note>
Only scans with a **Completed** status and ready graph data can be selected.
Scans that are still executing or have failed appear with disabled action
buttons.
</Note>
## Choosing a Query
After selecting a scan, the right panel activates a query dropdown. Each query targets a specific type of privilege escalation or misconfiguration pattern.
To choose a query, click the dropdown and select from the available options. Each option displays:
- **Query name:** A descriptive title (e.g., "IAM Privilege Escalation via AssumeRole")
- **Short description:** A brief summary of what the query detects
<img
src="/images/prowler-app/attack-paths/query-selector.png"
alt="Attack Paths query selector dropdown showing available queries"
width="700"
/>
Once selected, a description card appears below the dropdown with additional context about the query, including attribution links to external references when available.
## Configuring Query Parameters
Some queries accept optional or required parameters to narrow the scope of the analysis. When a query has parameters, a dynamic form appears below the query description.
- **Required fields** are marked with an asterisk (\*) and must be filled before executing
- **Optional fields** refine the query results but are not mandatory
If a query requires no parameters, the form displays a message confirming that the query is ready to execute.
<img
src="/images/prowler-app/attack-paths/query-parameters.png"
alt="Attack Paths query parameter form with fields"
width="700"
/>
## Executing a Query
To run the selected query against the scan data, click **Execute Query**. The button displays a loading state while the query processes.
If the query returns no results, an informational message appears. Common reasons include:
- **No matching patterns found:** The scanned environment does not contain the privilege escalation chain the query targets
- **Insufficient permissions:** The scan credentials may not have captured all the data the query needs
<img
src="/images/prowler-app/attack-paths/execute-query.png"
alt="Attack Paths right panel with query selected and execute button"
width="700"
/>
## Exploring the Graph
After a successful execution, the graph visualization renders below the query builder in a full-width panel. The graph maps relationships between cloud resources, IAM entities, and security findings.
### Node Types
- **Resource nodes** (rounded pills): Represent cloud resources such as IAM roles, policies, EC2 instances, and S3 buckets. Each resource type has a distinct color.
- **Finding nodes** (hexagons): Represent Prowler security findings linked to resources in the graph. Colors indicate severity level (critical, high, medium, low).
### Edge Types
- **Solid lines:** Direct relationships between resources (e.g., a role attached to a policy)
- **Dashed lines:** Connections between resources and their associated findings
A **legend** at the bottom of the graph lists all node types and edge types present in the current view.
<img
src="/images/prowler-app/attack-paths/graph-visualization.png"
alt="Attack Paths graph showing nodes, edges, and legend"
width="700"
/>
## Interacting with the Graph
### Filtering by Node
Click any node in the graph to filter the view and display only paths that pass through that node. When a filter is active:
- An information banner shows which node is selected
- Click **Back to Full View** to restore the complete graph
<img
src="/images/prowler-app/attack-paths/graph-filtered.png"
alt="Attack Paths graph filtered to show paths through a selected node"
width="700"
/>
### Graph Controls
The toolbar in the top-right corner of the graph provides:
- **Zoom in / Zoom out:** Adjust the zoom level
- **Fit to screen:** Reset the view to fit all nodes
- **Export:** Download the current graph as an SVG file
- **Fullscreen:** Open the graph in a full-screen modal with a side-by-side node detail panel
<Note>
Use **Ctrl + Scroll** (or **Cmd + Scroll** on macOS) to zoom directly within
the graph area.
</Note>
## Viewing Node Details
Click any node to open the **Node Details** panel below the graph. This panel displays:
- **Node type:** The resource category (e.g., "IAM Role," "EC2 Instance")
- **Properties:** All attributes of the selected node, including identifiers, timestamps, and configuration details
- **Related findings** (for resource nodes): A list of Prowler findings linked to the resource, with severity, title, and status
- **Affected resources** (for finding nodes): A list of resources associated with the finding
For finding nodes, a "View Finding" button links directly to the finding detail page for further investigation.
<img
src="/images/prowler-app/attack-paths/node-details.png"
alt="Attack Paths node detail panel showing properties and related findings"
width="700"
/>
## Fullscreen Mode
To expand the graph for detailed exploration, click the fullscreen icon in the graph toolbar. The fullscreen modal provides:
- The full graph visualization with all zoom and export controls
- A side panel for node details that appears when a node is selected
- All filtering and interaction capabilities available in the standard view
<img
src="/images/prowler-app/attack-paths/fullscreen-mode.png"
alt="Attack Paths fullscreen mode with graph and node detail side panel"
width="700"
/>
## Using Attack Paths with the MCP Server
Attack Paths capabilities are also available through the [Prowler MCP Server](/getting-started/products/prowler-mcp), enabling interaction with Attack Paths data via AI assistants like Claude Desktop, Cursor, and other MCP clients.
The following MCP tools are available for Attack Paths:
- **`prowler_app_list_attack_paths_scans`** - List and filter Attack Paths scans
- **`prowler_app_list_attack_paths_queries`** - Discover available queries for a completed scan
- **`prowler_app_run_attack_paths_query`** - Execute a query and retrieve graph results with nodes and relationships
These tools enable workflows such as:
- Asking an AI assistant to identify privilege escalation paths in a specific AWS account
- Automating attack path analysis across multiple scans
- Combining attack path data with findings and compliance information for comprehensive security reports
For the complete list of MCP tools, see the [Tools Reference](/getting-started/basic-usage/prowler-mcp-tools#attack-paths-analysis).
@@ -1,5 +1,5 @@
---
title: 'Import Findings'
title: 'Findings Ingestion'
description: 'Upload OCSF scan results to Prowler Cloud from external sources or the CLI'
---
@@ -10,7 +10,7 @@ import { VersionBadge } from "/snippets/version-badge.mdx"
Findings Ingestion enables uploading OCSF (Open Cybersecurity Schema Framework) scan results to Prowler Cloud. This feature supports importing findings from Prowler CLI output files that use the [Detection Finding](https://schema.ocsf.io/classes/detection_finding) class.
<Note>
This feature is available exclusively in **Prowler Cloud** with a paid subscription.
This feature is available exclusively in **Prowler Cloud**.
</Note>
## OCSF Detection Finding format
@@ -132,77 +132,69 @@ Only **Detection Finding** (`class_uid: 2004`) records are accepted. Other OCSF
## Required permissions
The **Manage Ingestions** RBAC permission controls access to the ingestion endpoints. Without this permission, findings cannot be submitted via the API or `--push-to-cloud`.
The **Manage Ingestions** RBAC permission controls access to the ingestion endpoints. Without this permission, findings cannot be submitted via the API or `--export-ocsf`.
For more information about RBAC permissions, refer to the [Prowler App RBAC documentation](/user-guide/tutorials/prowler-app-rbac).
## Using the CLI
The `--push-to-cloud` flag uploads scan results directly to Prowler Cloud after a scan completes. This approach automates the ingestion process without manual file uploads.
The `--export-ocsf` flag uploads scan results directly to Prowler Cloud after a scan completes. This approach automates the ingestion process without manual file uploads.
### Prerequisites
- A valid Prowler Cloud API key (see [API Keys](/user-guide/tutorials/prowler-app-api-keys))
- The `PROWLER_CLOUD_API_KEY` environment variable configured
- The `PROWLER_API_KEY` environment variable configured
### Basic usage
```bash
export PROWLER_CLOUD_API_KEY="pk_your_api_key_here"
export PROWLER_API_KEY="pk_your_api_key_here"
prowler aws --push-to-cloud
prowler aws --export-ocsf
```
### Combining with output formats
When using `--push-to-cloud` with custom output formats that exclude OCSF, Prowler generates a temporary OCSF file for upload:
When using `--export-ocsf` with custom output formats that exclude OCSF, Prowler generates a temporary OCSF file for upload:
The temporary OCSF file is saved in the system temporary directory and not in the output path passed with `-o`.
```bash
prowler aws --services accessanalyzer -M csv --push-to-cloud -o /tmp/scan-output
prowler aws --services accessanalyzer -M csv --export-ocsf -o /tmp/scan-output
```
When default output formats include OCSF, Prowler reuses the existing file. Default output formats include JSON-OCSF:
```bash
prowler aws --services accessanalyzer --push-to-cloud -o /tmp/scan-output
prowler aws --services accessanalyzer --export-ocsf -o /tmp/scan-output
```
### CLI output examples
**Successful upload:**
```
Pushing findings to Prowler Cloud, please wait...
Exporting OCSF to Prowler Cloud, please wait...
Findings successfully pushed to Prowler Cloud. Ingestion job: fa8bc8c5-4925-46a0-9fe0-f6575905e094
See more details here: https://cloud.prowler.com/scans
OCSF export accepted. Ingestion job: fa8bc8c5-4925-46a0-9fe0-f6575905e094
```
**Missing API key:**
```
Push to Prowler Cloud skipped: no API key configured. Set the PROWLER_CLOUD_API_KEY
WARNING: OCSF export skipped: no API key configured. Set the PROWLER_API_KEY
environment variable to enable it. Scan results were saved to
/tmp/scan-output/prowler-output-123456789012-20260217131755.ocsf.json
```
**API unreachable:**
```
Push to Prowler Cloud failed: could not reach the Prowler Cloud API at
WARNING: OCSF export skipped: could not reach the Prowler Cloud API at
https://api.prowler.com. Check the URL and your network connection. Scan results
were saved to /tmp/scan-output/prowler-output-123456789012-20260217131755.ocsf.json
```
**No subscription:**
```
Push to Prowler Cloud failed: this feature is only available with a Prowler Cloud
subscription. Scan results were saved to
/tmp/scan-output/prowler-output-123456789012-20260217131755.ocsf.json
```
**Invalid API key:**
```
Push to Prowler Cloud failed: the API returned HTTP 401. Verify your API key is
WARNING: OCSF export failed: the API returned HTTP 401. Verify your API key is
valid and has the right permissions. Scan results were saved to
/tmp/scan-output/prowler-output-123456789012-20260217131755.ocsf.json
```
@@ -220,10 +212,10 @@ The Ingestion API provides endpoints for submitting OCSF files and monitoring jo
Include the API key in the `Authorization` header:
```bash
export PROWLER_CLOUD_API_KEY="pk_your_api_key_here"
export PROWLER_API_KEY="pk_your_api_key_here"
curl -X POST \
-H "Authorization: Api-Key ${PROWLER_CLOUD_API_KEY}" \
-H "Authorization: Api-Key ${PROWLER_API_KEY}" \
-F "file=@/path/to/findings.ocsf.json" \
https://api.prowler.com/api/v1/ingestions
```
@@ -237,7 +229,7 @@ Upload a `.ocsf.json` file containing a JSON array of OCSF Detection Finding rec
**Request:**
```bash
curl -X POST \
-H "Authorization: Api-Key ${PROWLER_CLOUD_API_KEY}" \
-H "Authorization: Api-Key ${PROWLER_API_KEY}" \
-F "file=@scan-results.ocsf.json" \
https://api.prowler.com/api/v1/ingestions
```
@@ -275,7 +267,7 @@ Monitor the progress of an ingestion job.
**Request:**
```bash
curl -X GET \
-H "Authorization: Api-Key ${PROWLER_CLOUD_API_KEY}" \
-H "Authorization: Api-Key ${PROWLER_API_KEY}" \
-H "Accept: application/vnd.api+json" \
https://api.prowler.com/api/v1/ingestions/3650fef9-8e5f-4808-a95f-74f0afae8499
```
@@ -327,7 +319,7 @@ Retrieve a list of ingestion jobs for the tenant.
**Request:**
```bash
curl -X GET \
-H "Authorization: Api-Key ${PROWLER_CLOUD_API_KEY}" \
-H "Authorization: Api-Key ${PROWLER_API_KEY}" \
-H "Accept: application/vnd.api+json" \
"https://api.prowler.com/api/v1/ingestions?filter[status]=completed&page[size]=10"
```
@@ -341,7 +333,7 @@ Retrieve error details for a specific ingestion job.
**Request:**
```bash
curl -X GET \
-H "Authorization: Api-Key ${PROWLER_CLOUD_API_KEY}" \
-H "Authorization: Api-Key ${PROWLER_API_KEY}" \
-H "Accept: application/vnd.api+json" \
https://api.prowler.com/api/v1/ingestions/3650fef9-8e5f-4808-a95f-74f0afae8499/errors
```
@@ -371,9 +363,9 @@ Prowler must be installed in the CI/CD environment before running scans. Refer t
- name: Run Prowler and upload to Cloud
env:
PROWLER_CLOUD_API_KEY: ${{ secrets.PROWLER_CLOUD_API_KEY }}
PROWLER_API_KEY: ${{ secrets.PROWLER_API_KEY }}
run: |
prowler aws --services s3,iam --push-to-cloud
prowler aws --services s3,iam --export-ocsf
```
### GitLab CI
@@ -382,9 +374,9 @@ Prowler must be installed in the CI/CD environment before running scans. Refer t
prowler_scan:
script:
- pip install prowler
- prowler aws --services s3,iam --push-to-cloud
- prowler aws --services s3,iam --export-ocsf
variables:
PROWLER_CLOUD_API_KEY: $PROWLER_CLOUD_API_KEY
PROWLER_API_KEY: $PROWLER_API_KEY
```
## Billing impact
@@ -400,23 +392,6 @@ For pricing details, see [Prowler Cloud Pricing](https://prowler.com/pricing).
## Troubleshooting
### "Push to Prowler Cloud skipped: no API key configured"
- Set the `PROWLER_CLOUD_API_KEY` environment variable before running the scan
- Verify the variable is exported and not empty
### "Push to Prowler Cloud failed: could not reach the Prowler Cloud API"
- Verify network connectivity to `api.prowler.com`
- Check firewall rules allow outbound HTTPS traffic
- Confirm the API endpoint is not blocked by proxy settings
- If using a custom base URL via `PROWLER_CLOUD_API_BASE_URL`, verify it is correct
### "Push to Prowler Cloud failed: this feature is only available with a Prowler Cloud subscription"
- The API returned HTTP 402, meaning your tenant does not have an active subscription
- Visit [Prowler Cloud Pricing](https://prowler.com/pricing) to review available plans
### HTTP 401 Unauthorized
- Verify the API key is valid and not revoked
@@ -433,3 +408,9 @@ For pricing details, see [Prowler Cloud Pricing](https://prowler.com/pricing).
- Check the `/api/v1/ingestions/{id}/errors` endpoint for details
- Verify the OCSF file format is valid
- Ensure the file contains Detection Finding records
### CLI reports "could not reach the Prowler Cloud API"
- Verify network connectivity to `api.prowler.com`
- Check firewall rules allow outbound HTTPS traffic
- Confirm the API endpoint is not blocked by proxy settings
@@ -24,11 +24,6 @@ Advanced Mutelist enables users to create powerful, pattern-based muting rules u
## Prerequisites
<Note>
Advanced Mutelist requires the **Manage Account** permission. See [RBAC Administrative Permissions](/user-guide/tutorials/prowler-app-rbac#rbac-administrative-permissions) for details.
</Note>
Before muting findings, ensure:
- Valid access to Prowler App with appropriate permissions
@@ -77,10 +72,10 @@ If the YAML configuration is invalid, an error message will be displayed
2. Navigate to the Findings page to verify muted findings
![Check muted findings](/images/mutelist-ui-9.png)
<Warning>
The Advanced Mutelist configuration takes effect on **subsequent scans only**. Existing findings from previous scans are **not** retroactively muted. You must run a new scan after saving your YAML configuration to see its effect. Similarly, removing a pattern from the YAML configuration will only stop muting new findings generated by subsequent scans.
<Note>
The Advanced Mutelist configuration takes effect on subsequent scans. Existing findings are not retroactively muted.
</Warning>
</Note>
## YAML Configuration Examples
Below are ready-to-use examples for different cloud providers. For detailed syntax and logic explanation, see [CLI Mutelist documentation](/user-guide/cli/tutorials/mutelist#how-the-mutelist-works).
@@ -421,10 +416,6 @@ Mutelist:
Description: "Mute findings for dev/test environments in alpha project"
```
### Priority: Advanced vs. Simple Mutelist
When both Advanced Mutelist (YAML) and [Simple Mutelist](/user-guide/tutorials/prowler-app-simple-mutelist) rules match the same finding, the **Advanced Mutelist takes higher priority**. The finding will be muted with the reason "Muted by mutelist". If a finding is not matched by the Advanced Mutelist but matches a Simple Mutelist rule, the Simple rule's custom justification is used instead.
### Best Practices
1. **Start Small**: Begin with specific resources and gradually expand
@@ -238,6 +238,6 @@ To grant all administrative permissions, select the **Grant all admin permission
The following permissions are available exclusively in **Prowler Cloud**:
**Manage Ingestions:** Submit and manage findings ingestion jobs via the API. Required to upload OCSF scan results using the `--push-to-cloud` CLI flag or the ingestion endpoints. See [Import Findings](/user-guide/tutorials/prowler-app-import-findings) for details.
**Manage Ingestions:** Submit and manage findings ingestion jobs via the API. Required to upload OCSF scan results using the `--export-ocsf` CLI flag or the ingestion endpoints. See [Findings Ingestion](/user-guide/tutorials/prowler-app-findings-ingestion) for details.
**Manage Billing:** Access and manage billing settings, subscription plans, and payment methods.
@@ -23,11 +23,6 @@ Simple Mutelist creates rules based on the finding's unique identifier (UID). Fo
</Note>
<Note>
Simple Mutelist requires the **Manage Scans** permission. See [RBAC Administrative Permissions](/user-guide/tutorials/prowler-app-rbac#rbac-administrative-permissions) for details.
</Note>
## Accessing the Mutelist Page
To access the Mutelist page:
@@ -90,10 +85,10 @@ To toggle a mute rule without deleting it:
3. Locate the mute rule
4. Use the toggle switch in the "Enabled" column to enable or disable the rule
<Warning>
Disabling a mute rule does not retroactively unmute existing findings that were already marked as muted. Those findings retain their muted status as point-in-time historical records. Only **new findings** generated by subsequent scans will appear as unmuted.
<Note>
Disabled mute rules remain in the system but do not affect findings. Findings associated with disabled rules will appear as unmuted in subsequent scans.
</Warning>
</Note>
### Editing Mute Rules
@@ -117,7 +112,7 @@ To permanently remove a mute rule:
5. Confirm the deletion
<Warning>
Deleting a mute rule is permanent and cannot be undone. Existing findings that were already muted retain their muted status as historical records — only **new findings** from subsequent scans will appear as unmuted. To temporarily stop muting new findings without losing the rule, disable the rule instead of deleting it.
Deleting a mute rule is permanent. The finding will appear as unmuted in subsequent scans. To temporarily unmute a finding without losing the rule, disable the rule instead of deleting it.
</Warning>
@@ -129,13 +124,9 @@ Simple Mutelist creates mute rules based on a finding's unique identifier (UID).
- **Historical findings** with the same UID are also muted
- **Future findings** from subsequent scans are automatically muted if they match the UID
### Bulk Muting and Grouping
When muting multiple findings at once, a single mute rule is created containing all selected finding UIDs. However, once a rule is created, **additional findings cannot be added to an existing rule**. To mute new findings, create a separate mute rule.
### Uniqueness Constraint
Each finding UID can only belong to one **enabled** mute rule at a time. Attempting to create a mute rule that includes a finding UID already covered by another enabled rule displays a conflict error. If you need to reorganize mute rules, disable or delete the existing rule first, then create a new one.
Each finding UID can only have one mute rule. Attempting to create a duplicate mute rule for the same finding displays an error message indicating the rule already exists.
## Simple Mutelist vs. Advanced Mutelist
@@ -143,12 +134,8 @@ Each finding UID can only belong to one **enabled** mute rule at a time. Attempt
| ------------------------ | ----------------------------------------- | ------------------------------------------------------ |
| **Configuration method** | Point-and-click interface | YAML configuration file |
| **Muting scope** | Individual finding UIDs | Patterns based on checks, regions, resources, and tags |
| **When muting applies** | Immediately (current + historical findings) | On subsequent scans only (not retroactive) |
| **Unmuting behavior** | Disabling/deleting a rule only affects new findings from subsequent scans | Removing a pattern stops muting on the next scan |
| **Adding findings later** | Not supported — must create a new rule | Automatic — any finding matching the pattern is muted |
| **Regular expressions** | Not supported | Fully supported |
| **Bulk operations** | Checkbox selection in Findings table | YAML wildcards and patterns |
| **Priority** | Applied after Advanced Mutelist | Highest priority |
| **Best for** | Quick, ad-hoc muting of specific findings | Complex, policy-driven muting rules |
### When to Use Simple Mutelist
@@ -183,14 +170,6 @@ If an error indicates a mute rule already exists for a finding:
3. Edit the existing rule's justification if needed, or
4. Delete the existing rule and create a new one
### Finding Still Appears Muted After Disabling or Deleting a Rule
If a finding still appears as muted after disabling or deleting its mute rule:
1. This is expected behavior — existing findings retain their muted status as historical records
2. Run a new scan to generate new findings that will reflect the updated rule state
3. New findings with the same UID will appear with their actual status (PASS/FAIL) instead of muted
### Finding Still Appears Unmuted
If a muted finding still appears unmuted:
-12
View File
@@ -134,18 +134,6 @@ Track the progress of your scan in the `Scans` section:
<img src="/images/scan-progress.png" alt="Scan Progress" width="700" />
<Note>
**How Dashboards Display Scan Data**
Each dashboard handles scan data differently:
* **Overview** displays aggregated metrics from the **latest completed scan per provider** only.
* **Findings** displays results from the **latest completed scan per provider** by default. To access historical findings, apply a date or scan filter.
* **Resources** lists **all discovered resources across all scans**. However, when selecting a resource, the Findings tab within the resource detail shows only findings from the **latest completed scan**. If the latest scan did not evaluate a particular resource, its Findings tab may appear empty.
When a new scan completes or a new data ingestion is processed, the dashboards automatically reflect the updated results.
</Note>
## **Step 8: Analyze the Findings**
While the scan is running, start exploring the findings in these sections:
@@ -27,7 +27,7 @@ This feature is **exclusively available in Prowler Cloud**. For CLI-based multi-
Before using the AWS Organizations wizard, you need to deploy **two IAM roles** in your AWS environment. The onboarding follows this sequence:
<Frame>
<img src="/images/organizations/onboarding-flow.svg" alt="Onboarding flow: 1. Create Management Account Role (Quick Create or Manual), 2. Deploy StackSet, 3. Run the Wizard, 4. Launch Scans" />
<img src="/images/organizations/onboarding-flow.svg" alt="Onboarding flow: 1. Create Management Account Role, 2. Deploy StackSet, 3. Run the Wizard, 4. Launch Scans" />
</Frame>
## Key Concepts
@@ -46,11 +46,11 @@ Prowler requires **two separate IAM roles** deployed in different places, each w
| Role | Where it lives | What it does | How to deploy it |
|------|---------------|--------------|------------------|
| **ProwlerScan** (management account) | Your management (root) account only | Discovers the Organization structure **and** scans the management account. Has additional Organizations discovery permissions. | Via **Quick Create** link or **manually** in the IAM Console ([Step 1](#step-1-create-the-management-account-role)). Cannot be deployed via StackSet. |
| **ProwlerScan** (management account) | Your management (root) account only | Discovers the Organization structure **and** scans the management account. Has additional Organizations discovery permissions. | **Manually** in the IAM Console ([Step 1](#step-1-create-the-management-account-role)). Cannot be deployed via StackSet. |
| **ProwlerScan** (member accounts) | Every member account | Scans the account for security findings. | Via **CloudFormation StackSet** ([Step 2](#step-2-deploy-the-cloudformation-stackset)). Automated across all accounts. |
<Frame caption="Both roles share the same name `ProwlerScan`. The management account role includes additional Organization discovery permissions.">
<img src="/images/organizations/two-roles-architecture.svg" alt="Two Roles Architecture: ProwlerScan in management account (Quick Create or Manual, discovery + scanning) and ProwlerScan in member accounts (via StackSet, scanning only)" />
<img src="/images/organizations/two-roles-architecture.svg" alt="Two Roles Architecture: ProwlerScan in management account (discovery + scanning) and ProwlerScan in member accounts (scanning only)" />
</Frame>
<Note>
@@ -76,31 +76,14 @@ Your AWS environment must have [AWS Organizations](https://docs.aws.amazon.com/o
The first role you need to create is the **management account role**. This role allows Prowler to discover your Organization structure — listing accounts, OUs, and hierarchy.
<Warning>
**StackSets do not deploy to the management account.** Organizational CloudFormation StackSets with service-managed permissions only target member accounts — this is an AWS limitation, not a Prowler one. You must create the management account role separately, either via the Quick Create link ([Option A](#option-a-quick-create-link-fastest)) or manually ([Option B](#option-b-create-the-role-manually)).
**This role must be created manually.** Organizational CloudFormation StackSets do not deploy to the management account itself — this is an AWS limitation, not a Prowler one. StackSets with service-managed permissions only target member accounts. Similarly, the Prowler Quick Create link only deploys the role to member accounts.
</Warning>
<Note>
**The role must be named `ProwlerScan`** — the same name as the role deployed to member accounts via StackSet. Prowler expects a consistent role name across all accounts in the Organization. If you use a different name, connection tests and scans will fail for the management account.
</Note>
### Option A: Quick Create Link (Fastest)
The Prowler wizard provides a one-click link that opens the AWS Console with the CloudFormation template pre-configured. This creates a **CloudFormation Stack** (not a StackSet) that deploys the ProwlerScan role with Organizations permissions enabled in your management account.
<Tip>
**[Open Quick Create Stack in AWS Console →](https://us-east-1.console.aws.amazon.com/cloudformation/home?region=us-east-1#/stacks/quickcreate?templateURL=https%3A%2F%2Fprowler-cloud-public.s3.eu-west-1.amazonaws.com%2Fpermissions%2Ftemplates%2Faws%2Fcloudformation%2Fprowler-scan-role.yml&stackName=Prowler&param_EnableOrganizations=true)**
Opens the CloudFormation Console with the Prowler scan role template and `EnableOrganizations=true` pre-filled. You will need to enter the **ExternalId** parameter manually — copy it from the Prowler wizard ([Step 4](#step-4-authenticate-with-your-management-account)).
</Tip>
1. Click **[Open Quick Create Stack in AWS Console →](https://us-east-1.console.aws.amazon.com/cloudformation/home?region=us-east-1#/stacks/quickcreate?templateURL=https%3A%2F%2Fprowler-cloud-public.s3.eu-west-1.amazonaws.com%2Fpermissions%2Ftemplates%2Faws%2Fcloudformation%2Fprowler-scan-role.yml&stackName=Prowler&param_EnableOrganizations=true)** or use the **Create Stack in Management Account** button in the Prowler wizard (which also pre-fills the ExternalId).
2. Enter the **ExternalId** parameter if not pre-filled.
3. Check **"I acknowledge that AWS CloudFormation might create IAM resources with custom names"** and click **Create stack**.
4. Wait for the stack to reach **CREATE_COMPLETE** status.
Take note of the **Role ARN** from the stack's **Outputs** tab — you will need it in the wizard.
### Option B: Create the Role Manually
### Create the IAM Role
1. Sign in to the [AWS IAM Console](https://console.aws.amazon.com/iam/) in your **management account**.
@@ -198,26 +181,43 @@ The StackSet uses **service-managed permissions**, which means AWS Organizations
**Trusted access required:** CloudFormation StackSets must have trusted access enabled in your management account. Verify this in the AWS Console under **AWS Organizations > Settings > Trusted access for AWS CloudFormation StackSets**.
</Note>
<Warning>
**The Quick Create link creates a Stack, not a StackSet.** The link in the Prowler wizard creates a CloudFormation **Stack** that deploys the ProwlerScan role in your management account only ([Step 1](#step-1-create-the-management-account-role)). To deploy the role across **member accounts**, you must create a StackSet manually as described below. AWS does not support Quick Create links for StackSets.
</Warning>
### Option A: Using the Prowler Quick Create Link (Recommended)
The Prowler wizard provides a one-click link that opens the AWS Console with everything pre-configured.
<Tip>
**[Open StackSets Console →](https://us-east-1.console.aws.amazon.com/cloudformation/home?region=us-east-1#/stacksets/create)**
**[Open Quick Create in AWS Console →](https://us-east-1.console.aws.amazon.com/cloudformation/home?region=us-east-1#/stacks/quickcreate?templateURL=https%3A%2F%2Fprowler-cloud-public.s3.eu-west-1.amazonaws.com%2Fpermissions%2Ftemplates%2Faws%2Fcloudformation%2Fprowler-scan-role.yml&stackName=Prowler)**
Opens the CloudFormation StackSets creation page directly. You will need to paste the template URL and ExternalId manually.
Opens the CloudFormation Console with the Prowler scan role template and parameters pre-configured. You can also find this link in the Prowler wizard during [Step 4: Authentication](#step-4-authenticate-with-your-management-account).
</Tip>
1. Click the link above or navigate to **CloudFormation > StackSets > Create StackSet** in your management account.
2. Choose **Service-managed permissions**.
3. Select **Amazon S3 URL** as the template source and paste the following URL:
1. Review the pre-filled parameters:
- **Template URL**: Points to the official [Prowler scan role template](https://prowler-cloud-public.s3.eu-west-1.amazonaws.com/permissions/templates/aws/cloudformation/prowler-scan-role.yml) hosted on Prowler's public S3 bucket.
- **ExternalId**: Pre-filled with your tenant's External ID when clicking the link from the Prowler Cloud wizard. If you open this link directly, you will need to enter the External ID manually.
{/* TODO: screenshot of AWS Console Quick Create page showing pre-filled parameters */}
2. Under **Deployment targets**, select:
- **Deploy to organization** to deploy to all accounts, or
- **Deploy to organizational units (OUs)** and specify the OU IDs you want to cover.
3. Review the settings and click **Create StackSet**. AWS will begin deploying the ProwlerScan role to every target account.
### Option B: Manual StackSet Deployment
If you prefer full control over the deployment:
1. Open the [AWS CloudFormation Console](https://console.aws.amazon.com/cloudformation/) in your management account.
2. Go to **StackSets > Create StackSet**.
3. Choose **Service-managed permissions**.
4. Use this template URL:
```
https://prowler-cloud-public.s3.eu-west-1.amazonaws.com/permissions/templates/aws/cloudformation/prowler-scan-role.yml
```
4. Set the **ExternalId** parameter to the External ID shown in the Prowler wizard.
5. Choose your deployment targets (entire organization or specific OUs).
6. Select the AWS regions where you want the role deployed.
7. Click **Create StackSet**.
5. Set the **ExternalId** parameter to the External ID shown in the Prowler wizard.
6. Choose your deployment targets (entire organization or specific OUs).
7. Select the AWS regions where you want the role deployed.
8. Click **Create StackSet**.
### Verify StackSet Deployment
@@ -281,42 +281,32 @@ Click **Next** to proceed to the authentication phase.
## Step 4: Authenticate with Your Management Account
The wizard's **Authentication Details** page guides you through three actions: deploying the roles in AWS, entering the management account Role ARN, and confirming the deployment.
### Copy the External ID
### External ID
The wizard displays a **Prowler External ID** at the top — auto-generated and unique to your tenant. Click the copy icon to copy it. You will need this External ID for both the management account Stack and the member accounts StackSet.
### Deploy the Roles
The wizard provides two deployment actions:
1. **Create Stack in Management Account** — opens a Quick Create link that deploys the ProwlerScan role with `EnableOrganizations=true` in your management account ([Step 1](#step-1-create-the-management-account-role)). The External ID is pre-filled.
2. **Open StackSets Console** — links to the CloudFormation StackSets console where you create a StackSet for member accounts ([Step 2](#step-2-deploy-the-cloudformation-stackset)). Copy the template URL shown in the wizard and paste the External ID manually.
The wizard displays a **Prowler External ID** — auto-generated and unique to your tenant. Click the copy icon to copy it. If you haven't already configured the trust policy on your management account role ([Step 1](#step-1-create-the-management-account-role)), do so now using this External ID.
<Frame>
<img src="/images/organizations/authentication-details.png" alt="Authentication Details form showing External ID, two deployment buttons (Create Stack in Management Account and Open StackSets Console), Management Account Role ARN field, and deployment confirmation checkbox" />
<img src="/images/organizations/authentication-details.png" alt="Authentication Details form showing External ID, Role ARN field, and StackSet confirmation checkbox" />
</Frame>
### Enter the Management Account Role ARN
### Enter the Role ARN
Paste the **Role ARN** of the management account role you created in [Step 1](#step-1-create-the-management-account-role) into the **Management Account Role ARN** field.
Paste the **Role ARN** of the management account role you created in [Step 1](#step-1-create-the-management-account-role) into the **Role ARN** field.
The ARN follows this format:
```
arn:aws:iam::<account-id>:role/ProwlerScan
arn:aws:iam::<account-id>:role/<role-name>
```
For example: `arn:aws:iam::123456789012:role/ProwlerScan`
<Frame>
<img src="/images/organizations/role-arn-field.png" alt="Management Account Role ARN field in the Authentication Details form" />
<img src="/images/organizations/role-arn-field.png" alt="Role ARN field in the Authentication Details form" />
</Frame>
### Confirm and Discover
1. Check the box: **"The Stack and StackSet have been successfully deployed in AWS"**.
1. Check the box: **"The StackSet has been successfully deployed in AWS"**.
2. Click **Authenticate**.
Here's what happens behind the scenes:
@@ -324,6 +314,8 @@ Here's what happens behind the scenes:
- An asynchronous discovery is triggered to query your AWS Organization structure.
- You will see a **"Gathering AWS Accounts..."** spinner — this typically takes **30 seconds to 2 minutes** depending on your organization size.
{/* TODO: screenshot of the Authentication Details form with the spinner */}
## Step 5: Select Accounts to Scan
### Understanding the Tree View
@@ -372,6 +364,8 @@ Some accounts may appear as **blocked** (grayed out, not selectable). This happe
Hover over the blocked account to see the specific reason.
{/* TODO: screenshot of the tree view with account selection, showing active, already-connected, and blocked accounts */}
## Step 6: Test Connections
### How Connection Testing Works
@@ -458,6 +452,8 @@ Scans are only launched for accounts that are accessible (passed connection test
- Results populate the **Overview** and **Findings** pages.
- Prowler runs an **automatic sync every 6 hours** to detect new accounts added to your Organization or accounts that have been removed. New accounts are onboarded automatically based on the parent OU configuration.
{/* TODO: screenshot of the Launch Scan step */}
## Billing Impact
Each AWS account you connect through the Organizations wizard counts as one **provider** in your Prowler Cloud subscription.
+22 -28
View File
@@ -2,58 +2,52 @@
All notable changes to the **Prowler MCP Server** are documented in this file.
## [0.4.0] (Prowler v5.19.0)
## [0.4.0] (Prowler UNRELEASED)
### 🚀 Added
- Attack Paths tools to list scans, discover queries, and run Cypher queries against Neo4j [(#10145)](https://github.com/prowler-cloud/prowler/pull/10145)
---
- Add new MCP Server tools for Prowler Attack Paths [(#10145)](https://github.com/prowler-cloud/prowler/pull/10145)
## [0.3.0] (Prowler v5.16.0)
### 🚀 Added
### Added
- MCP Server tools for Prowler Compliance Framework Management [(#9568)](https://github.com/prowler-cloud/prowler/pull/9568)
- Add new MCP Server tools for Prowler Compliance Framework Management [(#9568)](https://github.com/prowler-cloud/prowler/pull/9568)
### 🔄 Changed
### Changed
- API base URL environment variable to include complete path [(#9542)](https://github.com/prowler-cloud/prowler/pull/9542)
- Prowler Hub and Docs tools format standardized for AI optimization [(#9578)](https://github.com/prowler-cloud/prowler/pull/9578)
---
- Update API base URL environment variable to include complete path [(#9542)](https://github.com/prowler-cloud/prowler/pull/9542)
- Standardize Prowler Hub and Docs tools format for AI optimization [(#9578)](https://github.com/prowler-cloud/prowler/pull/9578)
## [0.2.0] (Prowler v5.15.0)
### 🚀 Added
### Added
- MCP Server tools for Prowler Findings and Compliance, replacing all Prowler App MCP tools [(#9300)](https://github.com/prowler-cloud/prowler/pull/9300)
- MCP Server tools for Prowler Providers Management [(#9350)](https://github.com/prowler-cloud/prowler/pull/9350)
- MCP Server tools for Prowler Resources Management [(#9380)](https://github.com/prowler-cloud/prowler/pull/9380)
- MCP Server tools for Prowler Scans Management [(#9509)](https://github.com/prowler-cloud/prowler/pull/9509)
- MCP Server tools for Prowler Muting Management [(#9510)](https://github.com/prowler-cloud/prowler/pull/9510)
- Remove all Prowler App MCP tools; and add new MCP Server tools for Prowler Findings and Compliance [(#9300)](https://github.com/prowler-cloud/prowler/pull/9300)
- Add new MCP Server tools for Prowler Providers Management [(#9350)](https://github.com/prowler-cloud/prowler/pull/9350)
- Add new MCP Server tools for Prowler Resources Management [(#9380)](https://github.com/prowler-cloud/prowler/pull/9380)
- Add new MCP Server tools for Prowler Scans Management [(#9509)](https://github.com/prowler-cloud/prowler/pull/9509)
- Add new MCP Server tools for Prowler Muting Management [(#9510)](https://github.com/prowler-cloud/prowler/pull/9510)
---
## [0.1.1] (Prowler v5.14.0)
### 🐞 Fixed
### Fixed
- Documentation MCP Server to return list of dictionaries [(#9205)](https://github.com/prowler-cloud/prowler/pull/9205)
- Fix documentation MCP Server to return list of dictionaries [(#9205)](https://github.com/prowler-cloud/prowler/pull/9205)
---
## [0.1.0] (Prowler v5.13.0)
### 🚀 Added
### Added
- Initial release of Prowler MCP Server [(#8695)](https://github.com/prowler-cloud/prowler/pull/8695)
- Appropriate user-agent in requests [(#8724)](https://github.com/prowler-cloud/prowler/pull/8724)
- Set appropiate user-agent in requests [(#8724)](https://github.com/prowler-cloud/prowler/pull/8724)
- Basic logger functionality [(#8740)](https://github.com/prowler-cloud/prowler/pull/8740)
- MCP Server for Prowler Cloud and Prowler App (Self-Managed) APIs [(#8744)](https://github.com/prowler-cloud/prowler/pull/8744)
- Add new MCP Server for Prowler Cloud and Prowler App (Self-Managed) APIs [(#8744)](https://github.com/prowler-cloud/prowler/pull/8744)
- HTTP transport support [(#8784)](https://github.com/prowler-cloud/prowler/pull/8784)
- MCP Server for Prowler Documentation [(#8795)](https://github.com/prowler-cloud/prowler/pull/8795)
- Add new MCP Server for Prowler Documentation [(#8795)](https://github.com/prowler-cloud/prowler/pull/8795)
- API key support for STDIO mode and enhanced HTTP mode authentication [(#8823)](https://github.com/prowler-cloud/prowler/pull/8823)
- Health check endpoint [(#8905)](https://github.com/prowler-cloud/prowler/pull/8905)
- Prowler Documentation MCP Server updated to use Mintlify API [(#8916)](https://github.com/prowler-cloud/prowler/pull/8916)
- Custom production deployment using uvicorn [(#8958)](https://github.com/prowler-cloud/prowler/pull/8958)
- Add health check endpoint [(#8905)](https://github.com/prowler-cloud/prowler/pull/8905)
- Update Prowler Documentation MCP Server to use Mintlify API [(#8916)](https://github.com/prowler-cloud/prowler/pull/8916)
- Add custom production deployment using uvicorn [(#8958)](https://github.com/prowler-cloud/prowler/pull/8958)
+18 -63
View File
@@ -2,86 +2,40 @@
All notable changes to the **Prowler SDK** are documented in this file.
## [5.20.0] (Prowler v5.20.0)
## [5.19.0] (Prowler UNRELEASED)
### 🚀 Added
- `entra_conditional_access_policy_approved_client_app_required_for_mobile` check for M365 provider [(#10216)](https://github.com/prowler-cloud/prowler/pull/10216)
- `entra_conditional_access_policy_compliant_device_hybrid_joined_device_mfa_required` check for M365 provider [(#10197)](https://github.com/prowler-cloud/prowler/pull/10197)
- `trusted_ips` configurable option for `opensearch_service_domains_not_publicly_accessible` check to reduce false positives on IP-restricted policies [(#8631)](https://github.com/prowler-cloud/prowler/pull/8631)
- `guardduty_delegated_admin_enabled_all_regions` check for AWS provider [(#9867)](https://github.com/prowler-cloud/prowler/pull/9867)
- OpenStack object storage service with 7 checks [(#10258)](https://github.com/prowler-cloud/prowler/pull/10258)
- AWS Organizations OU metadata (OU ID, OU path) in ASFF, OCSF and CSV outputs [(#10283)](https://github.com/prowler-cloud/prowler/pull/10283)
### 🔄 Changed
- Update Kubernetes API server checks metadata to new format [(#9674)](https://github.com/prowler-cloud/prowler/pull/9674)
- Update Kubernetes Controller Manager service metadata to new format [(#9675)](https://github.com/prowler-cloud/prowler/pull/9675)
- Update Kubernetes Core service metadata to new format [(#9676)](https://github.com/prowler-cloud/prowler/pull/9676)
- Update Kubernetes Kubelet service metadata to new format [(#9677)](https://github.com/prowler-cloud/prowler/pull/9677)
- Update Kubernetes RBAC service metadata to new format [(#9678)](https://github.com/prowler-cloud/prowler/pull/9678)
- Update Kubernetes Scheduler service metadata to new format [(#9679)](https://github.com/prowler-cloud/prowler/pull/9679)
- Update MongoDB Atlas Organizations service metadata to new format [(#9658)](https://github.com/prowler-cloud/prowler/pull/9658)
- Update MongoDB Atlas clusters service metadata to new format [(#9657)](https://github.com/prowler-cloud/prowler/pull/9657)
- Update GitHub Repository service metadata to new format [(#9659)](https://github.com/prowler-cloud/prowler/pull/9659)
- Update GitHub Organization service metadata to new format [(#10273)](https://github.com/prowler-cloud/prowler/pull/10273)
- Update Oracle Cloud Compute Engine service metadata to new format [(#9371)](https://github.com/prowler-cloud/prowler/pull/9371)
- Update Oracle Cloud Database service metadata to new format [(#9372)](https://github.com/prowler-cloud/prowler/pull/9372)
- Update Oracle Cloud File Storage service metadata to new format [(#9374)](https://github.com/prowler-cloud/prowler/pull/9374)
- Update Oracle Cloud Integration service metadata to new format [(#9376)](https://github.com/prowler-cloud/prowler/pull/9376)
- Update Oracle Cloud KMS service metadata to new format [(#9377)](https://github.com/prowler-cloud/prowler/pull/9377)
- Update Oracle Cloud Network service metadata to new format [(#9378)](https://github.com/prowler-cloud/prowler/pull/9378)
- Update Oracle Cloud Object Storage service metadata to new format [(#9379)](https://github.com/prowler-cloud/prowler/pull/9379)
- Update Oracle Cloud Events service metadata to new format [(#9373)](https://github.com/prowler-cloud/prowler/pull/9373)
- Update Oracle Cloud Identity service metadata to new format [(#9375)](https://github.com/prowler-cloud/prowler/pull/9375)
- Update Alibaba Cloud services metadata to new format [(#10289)](https://github.com/prowler-cloud/prowler/pull/10289)
- Update M365 Admin Center service metadata to new format [(#9680)](https://github.com/prowler-cloud/prowler/pull/9680)
- Update M365 Defender service metadata to new format [(#9681)](https://github.com/prowler-cloud/prowler/pull/9681)
- Update M365 Purview service metadata to new format [(#9092)](https://github.com/prowler-cloud/prowler/pull/9092)
---
## [5.19.0] (Prowler v5.19.0)
### 🚀 Added
- `entra_authentication_method_sms_voice_disabled` check for M365 provider [(#10212)](https://github.com/prowler-cloud/prowler/pull/10212)
- `entra_default_app_management_policy_enabled` check for M365 provider [(#9898)](https://github.com/prowler-cloud/prowler/pull/9898)
- `Google Workspace` provider support with Directory service including 1 security check [(#10022)](https://github.com/prowler-cloud/prowler/pull/10022)
- `entra_conditional_access_policy_app_enforced_restrictions` check for M365 provider [(#10058)](https://github.com/prowler-cloud/prowler/pull/10058)
- `entra_app_registration_no_unused_privileged_permissions` check for M365 provider [(#10080)](https://github.com/prowler-cloud/prowler/pull/10080)
- `entra_app_enforced_restrictions` check for M365 provider [(#10058)](https://github.com/prowler-cloud/prowler/pull/10058)
- `entra_app_registration_no_unused_privileged_permissions` check for m365 provider [(#10080)](https://github.com/prowler-cloud/prowler/pull/10080)
- `defenderidentity_health_issues_no_open` check for M365 provider [(#10087)](https://github.com/prowler-cloud/prowler/pull/10087)
- `organization_verified_badge` check for GitHub provider [(#10033)](https://github.com/prowler-cloud/prowler/pull/10033)
- OpenStack provider `clouds_yaml_content` parameter for API integration [(#10003)](https://github.com/prowler-cloud/prowler/pull/10003)
- `defender_safe_attachments_policy_enabled` check for M365 provider [(#9833)](https://github.com/prowler-cloud/prowler/pull/9833)
- `defender_safelinks_policy_enabled` check for M365 provider [(#9832)](https://github.com/prowler-cloud/prowler/pull/9832)
- AI Skills: Added a skill for creating new Attack Paths queries in openCypher, compatible with Neo4j and Neptune [(#9975)](https://github.com/prowler-cloud/prowler/pull/9975)
- CSA CCM 4.0 for the AWS provider [(#10018)](https://github.com/prowler-cloud/prowler/pull/10018)
- CSA CCM 4.0 for the GCP provider [(#10042)](https://github.com/prowler-cloud/prowler/pull/10042)
- CSA CCM 4.0 for the Azure provider [(#10039)](https://github.com/prowler-cloud/prowler/pull/10039)
- CSA CCM 4.0 for the Oracle Cloud provider [(#10057)](https://github.com/prowler-cloud/prowler/pull/10057)
- OCI regions updater script and CI workflow [(#10020)](https://github.com/prowler-cloud/prowler/pull/10020)
- `image` provider for container image scanning with Trivy integration [(#9984)](https://github.com/prowler-cloud/prowler/pull/9984)
- OpenStack compute 7 new checks [(#9944)](https://github.com/prowler-cloud/prowler/pull/9944)
- CSA CCM 4.0 for the Alibaba Cloud provider [(#10061)](https://github.com/prowler-cloud/prowler/pull/10061)
- ECS Exec (ECS-006) privilege escalation detection via `ecs:ExecuteCommand` + `ecs:DescribeTasks` [(#10066)](https://github.com/prowler-cloud/prowler/pull/10066)
- `--export-ocsf` CLI flag to upload OCSF scan results to Prowler Cloud [(#10095)](https://github.com/prowler-cloud/prowler/pull/10095)
- `scan_id` field in OCSF `unmapped` output for ingestion correlation [(#10095)](https://github.com/prowler-cloud/prowler/pull/10095)
- `defenderxdr_endpoint_privileged_user_exposed_credentials` check for M365 provider [(#10084)](https://github.com/prowler-cloud/prowler/pull/10084)
- `defenderxdr_critical_asset_management_pending_approvals` check for M365 provider [(#10085)](https://github.com/prowler-cloud/prowler/pull/10085)
- `entra_seamless_sso_disabled` check for M365 provider [(#10086)](https://github.com/prowler-cloud/prowler/pull/10086)
- `entra_seamless_sso_disabled` check for m365 provider [(#10086)](https://github.com/prowler-cloud/prowler/pull/10086)
- Registry scan mode for `image` provider: enumerate and scan all images from OCI standard, Docker Hub, and ECR [(#9985)](https://github.com/prowler-cloud/prowler/pull/9985)
- File descriptor limits (`ulimits`) for Docker Compose worker services to prevent `Too many open files` errors [(#10107)](https://github.com/prowler-cloud/prowler/pull/10107)
- Add file descriptor limits (`ulimits`) to Docker Compose worker services to prevent `Too many open files` errors [(#10107)](https://github.com/prowler-cloud/prowler/pull/10107)
- SecNumCloud compliance framework for the AWS provider [(#10117)](https://github.com/prowler-cloud/prowler/pull/10117)
- CIS 6.0 for the AWS provider [(#10127)](https://github.com/prowler-cloud/prowler/pull/10127)
- `entra_conditional_access_policy_require_mfa_for_management_api` check for M365 provider [(#10150)](https://github.com/prowler-cloud/prowler/pull/10150)
- `entra_require_mfa_for_management_api` check for m365 provider [(#10150)](https://github.com/prowler-cloud/prowler/pull/10150)
- OpenStack provider multiple regions support [(#10135)](https://github.com/prowler-cloud/prowler/pull/10135)
- `entra_break_glass_account_fido2_security_key_registered` check for M365 provider [(#10213)](https://github.com/prowler-cloud/prowler/pull/10213)
- `entra_default_app_management_policy_enabled` check for M365 provider [(#9898)](https://github.com/prowler-cloud/prowler/pull/9898)
- OpenStack networking service with 6 security checks [(#9970)](https://github.com/prowler-cloud/prowler/pull/9970)
- OpenStack block storage service with 7 security checks [(#10120)](https://github.com/prowler-cloud/prowler/pull/10120)
- OpenStack compute service with 7 security checks [(#9944)](https://github.com/prowler-cloud/prowler/pull/9944)
- OpenStack image service with 6 security checks [(#10096)](https://github.com/prowler-cloud/prowler/pull/10096)
- `--provider-uid` CLI flag for IaC provider, used as `cloud.account.uid` in OCSF output and required with `--export-ocsf` [(#10233)](https://github.com/prowler-cloud/prowler/pull/10233)
- `unmapped.provider_uid` field in OCSF output to match CLI scan results with API provider entities during ingestion [(#10231)](https://github.com/prowler-cloud/prowler/pull/10231)
- `unmapped.provider` field in OCSF output for provider name availability in non-cloud providers like Kubernetes [(#10240)](https://github.com/prowler-cloud/prowler/pull/10240)
### 🔄 Changed
@@ -104,21 +58,13 @@ All notable changes to the **Prowler SDK** are documented in this file.
- Update Azure Entra ID service metadata to new format [(#9619)](https://github.com/prowler-cloud/prowler/pull/9619)
- Update Azure Virtual Machines service metadata to new format [(#9629)](https://github.com/prowler-cloud/prowler/pull/9629)
- Cloudflare provider credential validation with specific exceptions [(#9910)](https://github.com/prowler-cloud/prowler/pull/9910)
- Enhance AWS IAM privilege escalation detection with patterns from pathfinding.cloud library [(#9922)](https://github.com/prowler-cloud/prowler/pull/9922)
- Bump Trivy from 0.66.0 to 0.69.2 [(#10210)](https://github.com/prowler-cloud/prowler/pull/10210)
- Standardize GitHub and M365 provider account UIDs for consistent OCSF output [(#10226)](https://github.com/prowler-cloud/prowler/pull/10226)
- Standardize Cloudflare account and resource UIDs to prevent None values in findings [(#10227)](https://github.com/prowler-cloud/prowler/pull/10227)
### 🐞 Fixed
- Google Workspace provider `test_connection()` missing `provider_id` parameter for API integration [(#10247)](https://github.com/prowler-cloud/prowler/pull/10247)
- Update AWS checks metadata URLs to replace deprecated Trend Micro CloudOne Conformity (EOL July 2026) with Vision One and remove docs.prowler.com references [(#10068)](https://github.com/prowler-cloud/prowler/pull/10068)
- Standardize resource_id values across Azure checks to use actual Azure resource IDs and prevent duplicate resource entries [(#9994)](https://github.com/prowler-cloud/prowler/pull/9994)
- VPC endpoint service collection filtering third-party services that caused AccessDenied errors on `DescribeVpcEndpointServicePermissions` [(#10152)](https://github.com/prowler-cloud/prowler/pull/10152)
- Handle serialization errors in OCSF output for non-serializable resource metadata [(#10129)](https://github.com/prowler-cloud/prowler/pull/10129)
- Respect `AWS_ENDPOINT_URL` environment variable for STS session creation [(#10228)](https://github.com/prowler-cloud/prowler/pull/10228)
- Help text and typos in CLI flags [(#10040)](https://github.com/prowler-cloud/prowler/pull/10040)
- `elbv2_insecure_ssl_ciphers` false positive on AWS post-quantum (PQ) TLS policies like `ELBSecurityPolicy-TLS13-1-2-PQ-2025-09` [(#10219)](https://github.com/prowler-cloud/prowler/pull/10219)
### 🔐 Security
@@ -127,6 +73,14 @@ All notable changes to the **Prowler SDK** are documented in this file.
---
## [5.18.4] (Prowler v5.18.4)
### 🐞 Fixed
- Handle serialization errors in OCSF output for non-serializable resource metadata [(#10129)](https://github.com/prowler-cloud/prowler/pull/10129)
---
## [5.18.3] (Prowler v5.18.3)
### 🐞 Fixed
@@ -256,6 +210,7 @@ All notable changes to the **Prowler SDK** are documented in this file.
- Update Azure AI Search service metadata to new format [(#9087)](https://github.com/prowler-cloud/prowler/pull/9087)
- Update Azure AKS service metadata to new format [(#9611)](https://github.com/prowler-cloud/prowler/pull/9611)
- Update Azure API Management service metadata to new format [(#9612)](https://github.com/prowler-cloud/prowler/pull/9612)
- Enhance AWS IAM privilege escalation detection with patterns from pathfinding.cloud library [(#9922)](https://github.com/prowler-cloud/prowler/pull/9922)
### Fixed
+19 -27
View File
@@ -529,7 +529,7 @@ def prowler():
provider=global_provider, stats=stats
)
if getattr(args, "push_to_cloud", False):
if getattr(args, "export_ocsf", False):
if not ocsf_output or not getattr(ocsf_output, "file_path", None):
tmp_ocsf = tempfile.NamedTemporaryFile(
suffix=json_ocsf_file_suffix, delete=False
@@ -541,50 +541,42 @@ def prowler():
tmp_ocsf.close()
ocsf_output.batch_write_data_to_file()
print(
f"{Style.BRIGHT}\nPushing findings to Prowler Cloud, please wait...{Style.RESET_ALL}"
f"{Style.BRIGHT}\nExporting OCSF to Prowler Cloud, please wait...{Style.RESET_ALL}"
)
try:
response = send_ocsf_to_api(ocsf_output.file_path)
except ValueError:
print(
f"{Style.BRIGHT}{Fore.YELLOW}\nPush to Prowler Cloud skipped: no API key configured. "
"Set the PROWLER_CLOUD_API_KEY environment variable to enable it. "
f"Scan results were saved to {ocsf_output.file_path}{Style.RESET_ALL}"
logger.warning(
"OCSF export skipped: no API key configured. "
"Set the PROWLER_API_KEY environment variable to enable it. "
f"Scan results were saved to {ocsf_output.file_path}"
)
except requests.ConnectionError:
print(
f"{Style.BRIGHT}{Fore.RED}\nPush to Prowler Cloud failed: could not reach the Prowler Cloud API at "
logger.warning(
"OCSF export skipped: could not reach the Prowler Cloud API at "
f"{cloud_api_base_url}. Check the URL and your network connection. "
f"Scan results were saved to {ocsf_output.file_path}{Style.RESET_ALL}"
f"Scan results were saved to {ocsf_output.file_path}"
)
except requests.HTTPError as http_err:
if http_err.response.status_code == 402:
print(
f"{Style.BRIGHT}{Fore.RED}\nPush to Prowler Cloud failed: "
"this feature is only available with a Prowler Cloud subscription. "
f"Scan results were saved to {ocsf_output.file_path}{Style.RESET_ALL}"
)
else:
print(
f"{Style.BRIGHT}{Fore.RED}\nPush to Prowler Cloud failed: the API returned HTTP {http_err.response.status_code}. "
"Verify your API key is valid and has the right permissions. "
f"Scan results were saved to {ocsf_output.file_path}{Style.RESET_ALL}"
)
logger.warning(
f"OCSF export failed: the API returned HTTP {http_err.response.status_code}. "
"Verify your API key is valid and has the right permissions. "
f"Scan results were saved to {ocsf_output.file_path}"
)
except Exception as error:
print(
f"{Style.BRIGHT}{Fore.RED}\nPush to Prowler Cloud failed unexpectedly: {error}. "
f"Scan results were saved to {ocsf_output.file_path}{Style.RESET_ALL}"
logger.warning(
f"OCSF export failed unexpectedly: {error}. "
f"Scan results were saved to {ocsf_output.file_path}"
)
else:
job_id = response.get("data", {}).get("id") if response else None
if job_id:
print(
f"{Style.BRIGHT}{Fore.GREEN}\nFindings successfully pushed to Prowler Cloud. Ingestion job: {job_id}"
f"\nSee more details here: https://cloud.prowler.com/scans{Style.RESET_ALL}"
f"{Style.BRIGHT}{Fore.GREEN}\nOCSF export accepted. Ingestion job: {job_id}{Style.RESET_ALL}"
)
else:
logger.warning(
"Push to Prowler Cloud: unexpected API response (missing ingestion job ID). "
"OCSF export: unexpected API response (missing ingestion job ID). "
f"Scan results were saved to {ocsf_output.file_path}"
)
+19 -19
View File
@@ -74,7 +74,7 @@
}
],
"Checks": [
"entra_conditional_access_policy_require_mfa_for_management_api",
"entra_require_mfa_for_management_api",
"entra_policy_guest_invite_only_for_admin_roles",
"iam_custom_role_has_permissions_to_administer_resource_locks",
"iam_role_user_access_admin_restricted",
@@ -94,7 +94,7 @@
}
],
"Checks": [
"entra_conditional_access_policy_require_mfa_for_management_api",
"entra_require_mfa_for_management_api",
"entra_policy_default_users_cannot_create_security_groups",
"iam_custom_role_has_permissions_to_administer_resource_locks",
"iam_role_user_access_admin_restricted",
@@ -286,7 +286,7 @@
}
],
"Checks": [
"entra_conditional_access_policy_require_mfa_for_management_api",
"entra_require_mfa_for_management_api",
"entra_policy_default_users_cannot_create_security_groups",
"entra_policy_guest_invite_only_for_admin_roles",
"entra_policy_user_consent_for_verified_apps",
@@ -709,7 +709,7 @@
}
],
"Checks": [
"entra_conditional_access_policy_require_mfa_for_management_api",
"entra_require_mfa_for_management_api",
"entra_policy_guest_users_access_restrictions",
"entra_user_with_vm_access_has_mfa",
"iam_custom_role_has_permissions_to_administer_resource_locks",
@@ -2122,7 +2122,7 @@
"monitor_alert_delete_public_ip_address_rule",
"aks_clusters_public_access_disabled",
"app_function_access_keys_configured",
"entra_conditional_access_policy_require_mfa_for_management_api",
"entra_require_mfa_for_management_api",
"entra_policy_guest_users_access_restrictions",
"entra_user_with_vm_access_has_mfa",
"iam_role_user_access_admin_restricted",
@@ -3497,7 +3497,7 @@
}
],
"Checks": [
"entra_conditional_access_policy_require_mfa_for_management_api",
"entra_require_mfa_for_management_api",
"entra_user_with_vm_access_has_mfa",
"iam_custom_role_has_permissions_to_administer_resource_locks",
"iam_role_user_access_admin_restricted",
@@ -4522,7 +4522,7 @@
}
],
"Checks": [
"entra_conditional_access_policy_require_mfa_for_management_api",
"entra_require_mfa_for_management_api",
"entra_policy_guest_invite_only_for_admin_roles",
"entra_policy_guest_users_access_restrictions",
"entra_user_with_vm_access_has_mfa",
@@ -4894,7 +4894,7 @@
}
],
"Checks": [
"entra_conditional_access_policy_require_mfa_for_management_api",
"entra_require_mfa_for_management_api",
"entra_policy_guest_users_access_restrictions",
"entra_user_with_vm_access_has_mfa",
"iam_custom_role_has_permissions_to_administer_resource_locks",
@@ -4917,7 +4917,7 @@
}
],
"Checks": [
"entra_conditional_access_policy_require_mfa_for_management_api",
"entra_require_mfa_for_management_api",
"entra_policy_guest_users_access_restrictions",
"entra_user_with_vm_access_has_mfa",
"iam_custom_role_has_permissions_to_administer_resource_locks",
@@ -5053,7 +5053,7 @@
}
],
"Checks": [
"entra_conditional_access_policy_require_mfa_for_management_api",
"entra_require_mfa_for_management_api",
"entra_privileged_user_has_mfa",
"entra_user_with_vm_access_has_mfa"
]
@@ -5298,7 +5298,7 @@
}
],
"Checks": [
"entra_conditional_access_policy_require_mfa_for_management_api",
"entra_require_mfa_for_management_api",
"entra_privileged_user_has_mfa",
"entra_user_with_vm_access_has_mfa",
"iam_role_user_access_admin_restricted",
@@ -5346,7 +5346,7 @@
}
],
"Checks": [
"entra_conditional_access_policy_require_mfa_for_management_api",
"entra_require_mfa_for_management_api",
"entra_policy_guest_users_access_restrictions",
"entra_policy_user_consent_for_verified_apps",
"entra_user_with_vm_access_has_mfa",
@@ -5429,7 +5429,7 @@
}
],
"Checks": [
"entra_conditional_access_policy_require_mfa_for_management_api",
"entra_require_mfa_for_management_api",
"entra_user_with_vm_access_has_mfa"
]
},
@@ -5518,7 +5518,7 @@
}
],
"Checks": [
"entra_conditional_access_policy_require_mfa_for_management_api",
"entra_require_mfa_for_management_api",
"entra_non_privileged_user_has_mfa",
"entra_user_with_vm_access_has_mfa"
]
@@ -5557,7 +5557,7 @@
"app_function_not_publicly_accessible",
"containerregistry_not_publicly_accessible",
"containerregistry_uses_private_link",
"entra_conditional_access_policy_require_mfa_for_management_api",
"entra_require_mfa_for_management_api",
"entra_policy_guest_users_access_restrictions",
"entra_user_with_vm_access_has_mfa",
"iam_role_user_access_admin_restricted",
@@ -5598,7 +5598,7 @@
"app_function_not_publicly_accessible",
"containerregistry_not_publicly_accessible",
"containerregistry_uses_private_link",
"entra_conditional_access_policy_require_mfa_for_management_api",
"entra_require_mfa_for_management_api",
"entra_policy_guest_users_access_restrictions",
"entra_user_with_vm_access_has_mfa",
"iam_role_user_access_admin_restricted",
@@ -9010,7 +9010,7 @@
}
],
"Checks": [
"entra_conditional_access_policy_require_mfa_for_management_api",
"entra_require_mfa_for_management_api",
"entra_non_privileged_user_has_mfa",
"entra_privileged_user_has_mfa",
"entra_user_with_vm_access_has_mfa"
@@ -9029,7 +9029,7 @@
}
],
"Checks": [
"entra_conditional_access_policy_require_mfa_for_management_api",
"entra_require_mfa_for_management_api",
"entra_privileged_user_has_mfa"
]
},
@@ -9240,7 +9240,7 @@
}
],
"Checks": [
"entra_conditional_access_policy_require_mfa_for_management_api",
"entra_require_mfa_for_management_api",
"entra_policy_guest_invite_only_for_admin_roles",
"entra_policy_guest_users_access_restrictions",
"iam_custom_role_has_permissions_to_administer_resource_locks",
+10 -10
View File
@@ -1414,7 +1414,7 @@
}
],
"Checks": [
"entra_conditional_access_policy_require_mfa_for_management_api",
"entra_require_mfa_for_management_api",
"entra_global_admin_in_less_than_five_users",
"entra_privileged_user_has_mfa",
"iam_role_user_access_admin_restricted",
@@ -5135,7 +5135,7 @@
"Checks": [
"entra_non_privileged_user_has_mfa",
"entra_privileged_user_has_mfa",
"entra_conditional_access_policy_require_mfa_for_management_api",
"entra_require_mfa_for_management_api",
"entra_security_defaults_enabled",
"entra_user_with_vm_access_has_mfa"
]
@@ -5201,7 +5201,7 @@
}
],
"Checks": [
"entra_conditional_access_policy_require_mfa_for_management_api",
"entra_require_mfa_for_management_api",
"entra_non_privileged_user_has_mfa",
"entra_privileged_user_has_mfa",
"entra_security_defaults_enabled"
@@ -5266,7 +5266,7 @@
}
],
"Checks": [
"entra_conditional_access_policy_require_mfa_for_management_api",
"entra_require_mfa_for_management_api",
"entra_non_privileged_user_has_mfa",
"entra_privileged_user_has_mfa",
"entra_security_defaults_enabled"
@@ -5331,7 +5331,7 @@
}
],
"Checks": [
"entra_conditional_access_policy_require_mfa_for_management_api",
"entra_require_mfa_for_management_api",
"entra_non_privileged_user_has_mfa",
"entra_privileged_user_has_mfa",
"entra_user_with_vm_access_has_mfa"
@@ -5411,7 +5411,7 @@
"keyvault_rbac_enabled",
"keyvault_private_endpoints",
"keyvault_access_only_through_private_endpoints",
"entra_conditional_access_policy_require_mfa_for_management_api",
"entra_require_mfa_for_management_api",
"entra_global_admin_in_less_than_five_users",
"entra_non_privileged_user_has_mfa",
"entra_security_defaults_enabled",
@@ -5506,7 +5506,7 @@
"aks_clusters_public_access_disabled",
"app_function_not_publicly_accessible",
"entra_global_admin_in_less_than_five_users",
"entra_conditional_access_policy_require_mfa_for_management_api",
"entra_require_mfa_for_management_api",
"entra_non_privileged_user_has_mfa",
"entra_privileged_user_has_mfa",
"entra_trusted_named_locations_exists",
@@ -5571,7 +5571,7 @@
}
],
"Checks": [
"entra_conditional_access_policy_require_mfa_for_management_api",
"entra_require_mfa_for_management_api",
"entra_global_admin_in_less_than_five_users",
"entra_non_privileged_user_has_mfa",
"iam_role_user_access_admin_restricted",
@@ -5681,7 +5681,7 @@
"network_ssh_internet_access_restricted",
"network_udp_internet_access_restricted",
"vm_jit_access_enabled",
"entra_conditional_access_policy_require_mfa_for_management_api",
"entra_require_mfa_for_management_api",
"entra_global_admin_in_less_than_five_users",
"entra_non_privileged_user_has_mfa",
"entra_privileged_user_has_mfa",
@@ -5845,7 +5845,7 @@
"entra_global_admin_in_less_than_five_users",
"entra_non_privileged_user_has_mfa",
"entra_privileged_user_has_mfa",
"entra_conditional_access_policy_require_mfa_for_management_api",
"entra_require_mfa_for_management_api",
"keyvault_rbac_enabled",
"vm_jit_access_enabled",
"vm_linux_enforce_ssh_authentication"
+1 -1
View File
@@ -688,7 +688,7 @@
"Id": "1.2.6",
"Description": "Ensure Multifactor Authentication is Required for Windows Azure Service Management API",
"Checks": [
"entra_conditional_access_policy_require_mfa_for_management_api"
"entra_require_mfa_for_management_api"
],
"Attributes": [
{
+1 -1
View File
@@ -729,7 +729,7 @@
"Id": "2.2.7",
"Description": "Ensure Multi-factor Authentication is Required for Windows Azure Service Management API",
"Checks": [
"entra_conditional_access_policy_require_mfa_for_management_api"
"entra_require_mfa_for_management_api"
],
"Attributes": [
{
+1 -1
View File
@@ -1013,7 +1013,7 @@
"Id": "6.2.6",
"Description": "Ensure that multifactor authentication is required for Windows Azure Service Management API",
"Checks": [
"entra_conditional_access_policy_require_mfa_for_management_api"
"entra_require_mfa_for_management_api"
],
"Attributes": [
{
+1 -1
View File
@@ -449,7 +449,7 @@
"Id": "5.2.6",
"Description": "Ensure that multifactor authentication is required for Windows Azure Service Management API",
"Checks": [
"entra_conditional_access_policy_require_mfa_for_management_api"
"entra_require_mfa_for_management_api"
],
"Attributes": [
{
@@ -3703,7 +3703,7 @@
}
],
"Checks": [
"entra_conditional_access_policy_require_mfa_for_management_api",
"entra_require_mfa_for_management_api",
"entra_global_admin_in_less_than_five_users",
"entra_user_with_vm_access_has_mfa",
"iam_role_user_access_admin_restricted",
@@ -3921,7 +3921,7 @@
}
],
"Checks": [
"entra_conditional_access_policy_require_mfa_for_management_api",
"entra_require_mfa_for_management_api",
"entra_non_privileged_user_has_mfa",
"entra_privileged_user_has_mfa",
"entra_security_defaults_enabled",
@@ -279,7 +279,7 @@
}
],
"Checks": [
"entra_conditional_access_policy_require_mfa_for_management_api"
"entra_require_mfa_for_management_api"
]
},
{
@@ -329,7 +329,7 @@
}
],
"Checks": [
"entra_conditional_access_policy_require_mfa_for_management_api"
"entra_require_mfa_for_management_api"
]
},
{
@@ -484,7 +484,7 @@
}
],
"Checks": [
"entra_conditional_access_policy_require_mfa_for_management_api"
"entra_require_mfa_for_management_api"
]
},
{
@@ -89,7 +89,7 @@
}
],
"Checks": [
"entra_conditional_access_policy_require_mfa_for_management_api",
"entra_require_mfa_for_management_api",
"entra_global_admin_in_less_than_five_users",
"entra_non_privileged_user_has_mfa",
"entra_policy_default_users_cannot_create_security_groups",
+2 -2
View File
@@ -142,7 +142,7 @@
"entra_privileged_user_has_mfa",
"entra_non_privileged_user_has_mfa",
"entra_security_defaults_enabled",
"entra_conditional_access_policy_require_mfa_for_management_api",
"entra_require_mfa_for_management_api",
"entra_user_with_vm_access_has_mfa",
"network_flow_log_captured_sent",
"app_http_logs_enabled"
@@ -730,7 +730,7 @@
"entra_security_defaults_enabled",
"entra_privileged_user_has_mfa",
"entra_non_privileged_user_has_mfa",
"entra_conditional_access_policy_require_mfa_for_management_api",
"entra_require_mfa_for_management_api",
"entra_user_with_vm_access_has_mfa",
"entra_trusted_named_locations_exists",
"sqlserver_azuread_administrator_enabled",
@@ -35,7 +35,7 @@
}
],
"Checks": [
"entra_conditional_access_policy_require_mfa_for_management_api",
"entra_require_mfa_for_management_api",
"entra_global_admin_in_less_than_five_users",
"entra_non_privileged_user_has_mfa",
"entra_policy_default_users_cannot_create_security_groups",
@@ -307,7 +307,7 @@
}
],
"Checks": [
"entra_conditional_access_policy_require_mfa_for_management_api",
"entra_require_mfa_for_management_api",
"entra_non_privileged_user_has_mfa",
"entra_privileged_user_has_mfa",
"entra_user_with_vm_access_has_mfa"

Some files were not shown because too many files have changed in this diff Show More