Merge branch 'master' into PROWLER-182-kubernetes-add-and-connect-the-provider

This commit is contained in:
StylusFrost authored and GitHub committed 2025-11-19 14:07:57 +01:00
commit 054f85d7ba
569 files changed
+31875 -13155

No files matched your search

+3 -1
View File
@@ -14,6 +14,7 @@ UI_PORT=3000
AUTH_SECRET="N/c6mnaS5+SWq81+819OrzQZlmx1Vxtp/orjttJSmw8="
# Google Tag Manager ID
NEXT_PUBLIC_GOOGLE_TAG_MANAGER_ID=""
#### Code Review Configuration ####
# Enable Claude Code standards validation on pre-push hook
# Set to 'true' to validate changes against AGENTS.md standards via Claude Code
@@ -107,6 +108,8 @@ DJANGO_THROTTLE_TOKEN_OBTAIN=50/minute
# Sentry settings
SENTRY_ENVIRONMENT=local
SENTRY_RELEASE=local
NEXT_PUBLIC_SENTRY_ENVIRONMENT=${SENTRY_ENVIRONMENT}
#### Prowler release version ####
NEXT_PUBLIC_PROWLER_RELEASE_VERSION=v5.12.2
@@ -137,4 +140,3 @@ LANGCHAIN_PROJECT=""
RSS_FEED_SOURCES='[{"id":"prowler-releases","name":"Prowler Releases","type":"github_releases","url":"https://github.com/prowler-cloud/prowler/releases.atom","enabled":true}]'
# Example with multiple sources (no trailing comma after last item):
# RSS_FEED_SOURCES='[{"id":"prowler-releases","name":"Prowler Releases","type":"github_releases","url":"https://github.com/prowler-cloud/prowler/releases.atom","enabled":true},{"id":"prowler-blog","name":"Prowler Blog","type":"blog","url":"https://prowler.com/blog/rss","enabled":false}]'
+62 -20
View File
@@ -44,7 +44,16 @@ jobs:
container-build-push:
needs: setup
runs-on: ubuntu-latest
runs-on: ${{ matrix.runner }}
strategy:
matrix:
include:
- platform: linux/amd64
runner: ubuntu-latest
arch: amd64
- platform: linux/arm64
runner: ubuntu-24.04-arm
arch: arm64
timeout-minutes: 30
permissions:
contents: read
@@ -63,17 +72,17 @@ jobs:
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1
- name: Build and push API container (latest)
if: github.event_name == 'push'
- name: Build and push API container for ${{ matrix.arch }}
if: github.event_name == 'push' || github.event_name == 'release'
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0
with:
context: ${{ env.WORKING_DIRECTORY }}
push: true
platforms: ${{ matrix.platform }}
tags: |
${{ env.PROWLERCLOUD_DOCKERHUB_REPOSITORY }}/${{ env.PROWLERCLOUD_DOCKERHUB_IMAGE }}:${{ env.LATEST_TAG }}
${{ env.PROWLERCLOUD_DOCKERHUB_REPOSITORY }}/${{ env.PROWLERCLOUD_DOCKERHUB_IMAGE }}:${{ needs.setup.outputs.short-sha }}
cache-from: type=gha
cache-to: type=gha,mode=max
${{ env.PROWLERCLOUD_DOCKERHUB_REPOSITORY }}/${{ env.PROWLERCLOUD_DOCKERHUB_IMAGE }}:${{ needs.setup.outputs.short-sha }}-${{ matrix.arch }}
cache-from: type=gha,scope=${{ matrix.arch }}
cache-to: type=gha,mode=max,scope=${{ matrix.arch }}
- name: Notify container push started
if: github.event_name == 'release'
@@ -89,19 +98,6 @@ jobs:
slack-bot-token: ${{ secrets.SLACK_BOT_TOKEN }}
payload-file-path: "./.github/scripts/slack-messages/container-release-started.json"
- name: Build and push API container (release)
if: github.event_name == 'release'
id: container-push
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0
with:
context: ${{ env.WORKING_DIRECTORY }}
push: true
tags: |
${{ env.PROWLERCLOUD_DOCKERHUB_REPOSITORY }}/${{ env.PROWLERCLOUD_DOCKERHUB_IMAGE }}:${{ env.RELEASE_TAG }}
${{ env.PROWLERCLOUD_DOCKERHUB_REPOSITORY }}/${{ env.PROWLERCLOUD_DOCKERHUB_IMAGE }}:${{ env.STABLE_TAG }}
cache-from: type=gha
cache-to: type=gha,mode=max
- name: Notify container push completed
if: github.event_name == 'release' && always()
uses: ./.github/actions/slack-notification
@@ -117,6 +113,52 @@ jobs:
payload-file-path: "./.github/scripts/slack-messages/container-release-completed.json"
step-outcome: ${{ steps.container-push.outcome }}
# Create and push multi-architecture manifest
create-manifest:
needs: [setup, container-build-push]
if: github.event_name == 'push' || github.event_name == 'release'
runs-on: ubuntu-latest
steps:
- name: Login to DockerHub
uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1
- name: Create and push manifests for push event
if: github.event_name == 'push'
run: |
docker buildx imagetools create \
-t ${{ env.PROWLERCLOUD_DOCKERHUB_REPOSITORY }}/${{ env.PROWLERCLOUD_DOCKERHUB_IMAGE }}:${{ env.LATEST_TAG }} \
-t ${{ env.PROWLERCLOUD_DOCKERHUB_REPOSITORY }}/${{ env.PROWLERCLOUD_DOCKERHUB_IMAGE }}:${{ needs.setup.outputs.short-sha }} \
${{ env.PROWLERCLOUD_DOCKERHUB_REPOSITORY }}/${{ env.PROWLERCLOUD_DOCKERHUB_IMAGE }}:${{ needs.setup.outputs.short-sha }}-amd64 \
${{ env.PROWLERCLOUD_DOCKERHUB_REPOSITORY }}/${{ env.PROWLERCLOUD_DOCKERHUB_IMAGE }}:${{ needs.setup.outputs.short-sha }}-arm64
- name: Create and push manifests for release event
if: github.event_name == 'release'
run: |
docker buildx imagetools create \
-t ${{ env.PROWLERCLOUD_DOCKERHUB_REPOSITORY }}/${{ env.PROWLERCLOUD_DOCKERHUB_IMAGE }}:${{ env.RELEASE_TAG }} \
-t ${{ env.PROWLERCLOUD_DOCKERHUB_REPOSITORY }}/${{ env.PROWLERCLOUD_DOCKERHUB_IMAGE }}:${{ env.STABLE_TAG }} \
${{ env.PROWLERCLOUD_DOCKERHUB_REPOSITORY }}/${{ env.PROWLERCLOUD_DOCKERHUB_IMAGE }}:${{ needs.setup.outputs.short-sha }}-amd64 \
${{ env.PROWLERCLOUD_DOCKERHUB_REPOSITORY }}/${{ env.PROWLERCLOUD_DOCKERHUB_IMAGE }}:${{ needs.setup.outputs.short-sha }}-arm64
- name: Install regctl
if: always()
uses: regclient/actions/regctl-installer@f61d18f46c86af724a9c804cb9ff2a6fec741c7c # main
- name: Cleanup intermediate architecture tags
if: always()
run: |
echo "Cleaning up intermediate tags..."
regctl tag delete "${{ env.PROWLERCLOUD_DOCKERHUB_REPOSITORY }}/${{ env.PROWLERCLOUD_DOCKERHUB_IMAGE }}:${{ needs.setup.outputs.short-sha }}-amd64" || true
regctl tag delete "${{ env.PROWLERCLOUD_DOCKERHUB_REPOSITORY }}/${{ env.PROWLERCLOUD_DOCKERHUB_IMAGE }}:${{ needs.setup.outputs.short-sha }}-arm64" || true
echo "Cleanup completed"
trigger-deployment:
if: github.event_name == 'push'
needs: [setup, container-build-push]
@@ -0,0 +1,39 @@
name: 'Tools: Comment Label Update'
on:
issue_comment:
types:
- 'created'
concurrency:
group: ${{ github.workflow }}-${{ github.event.issue.number }}
cancel-in-progress: false
jobs:
update-labels:
if: contains(github.event.issue.labels.*.name, 'status/awaiting-response')
runs-on: ubuntu-latest
timeout-minutes: 5
permissions:
issues: write
pull-requests: write
steps:
- name: Remove 'status/awaiting-response' label
env:
GH_TOKEN: ${{ github.token }}
ISSUE_NUMBER: ${{ github.event.issue.number }}
run: |
echo "Removing 'status/awaiting-response' label from #$ISSUE_NUMBER"
gh api /repos/${{ github.repository }}/issues/$ISSUE_NUMBER/labels/status%2Fawaiting-response \
-X DELETE
- name: Add 'status/waiting-for-revision' label
env:
GH_TOKEN: ${{ github.token }}
ISSUE_NUMBER: ${{ github.event.issue.number }}
run: |
echo "Adding 'status/waiting-for-revision' label to #$ISSUE_NUMBER"
gh api /repos/${{ github.repository }}/issues/$ISSUE_NUMBER/labels \
-X POST \
-f labels[]='status/waiting-for-revision'
-45
View File
@@ -1,45 +0,0 @@
name: Community PR labelling
on:
# We need "write" permissions on the PR to be able to add a label.
pull_request_target: # We need this to have labelling permissions. There are no user inputs here, so we should be fine.
types:
- opened
permissions: {}
jobs:
label-if-community:
name: Add 'community' label if the PR is from a community contributor
if: github.repository == 'prowler-cloud/prowler'
runs-on: ubuntu-latest
permissions:
pull-requests: write
steps:
- name: Check if author is org member
id: check_membership
env:
GH_TOKEN: ${{ github.token }}
AUTHOR: ${{ github.event.pull_request.user.login }}
ORG: ${{ github.repository_owner }}
run: |
echo "Checking if $AUTHOR is a member of $ORG"
if gh api --method GET "orgs/$ORG/members/$AUTHOR" >/dev/null 2>&1; then
echo "is_member=true" >> $GITHUB_OUTPUT
echo "$AUTHOR is an organization member"
else
echo "is_member=false" >> $GITHUB_OUTPUT
echo "$AUTHOR is not an organization member"
fi
- name: Add community label
if: steps.check_membership.outputs.is_member == 'false'
env:
PR_NUMBER: ${{ github.event.pull_request.number }}
GH_TOKEN: ${{ github.token }}
run: |
echo "Adding 'community' label to PR #$PR_NUMBER"
gh api /repos/${{ github.repository }}/issues/${{ github.event.number }}/labels \
-X POST \
-f labels[]='community'
+63
View File
@@ -27,3 +27,66 @@ jobs:
uses: actions/labeler@634933edcd8ababfe52f92936142cc22ac488b1b # v6.0.1
with:
sync-labels: true
label-community:
name: Add 'community' label if the PR is from a community contributor
needs: labeler
if: github.repository == 'prowler-cloud/prowler' && github.event.action == 'opened'
runs-on: ubuntu-latest
permissions:
pull-requests: write
steps:
- name: Check if author is org member
id: check_membership
env:
AUTHOR: ${{ github.event.pull_request.user.login }}
run: |
# Hardcoded list of prowler-cloud organization members
# This list includes members who have set their organization membership as private
ORG_MEMBERS=(
"AdriiiPRodri"
"Alan-TheGentleman"
"alejandrobailo"
"amitsharm"
"andoniaf"
"cesararroba"
"Chan9390"
"danibarranqueroo"
"HugoPBrito"
"jfagoagas"
"josemazo"
"lydiavilchez"
"mmuller88"
"MrCloudSec"
"pedrooot"
"prowler-bot"
"puchy22"
"rakan-pro"
"RosaRivasProwler"
"StylusFrost"
"toniblyx"
"vicferpoy"
)
echo "Checking if $AUTHOR is a member of prowler-cloud organization"
# Check if author is in the org members list
if printf '%s\n' "${ORG_MEMBERS[@]}" | grep -q "^${AUTHOR}$"; then
echo "is_member=true" >> $GITHUB_OUTPUT
echo "$AUTHOR is an organization member"
else
echo "is_member=false" >> $GITHUB_OUTPUT
echo "$AUTHOR is not an organization member"
fi
- name: Add community label
if: steps.check_membership.outputs.is_member == 'false'
env:
PR_NUMBER: ${{ github.event.pull_request.number }}
GH_TOKEN: ${{ github.token }}
run: |
echo "Adding 'community' label to PR #$PR_NUMBER"
gh api /repos/${{ github.repository }}/issues/${{ github.event.number }}/labels \
-X POST \
-f labels[]='community'
+64 -29
View File
@@ -43,7 +43,16 @@ jobs:
container-build-push:
needs: setup
runs-on: ubuntu-latest
runs-on: ${{ matrix.runner }}
strategy:
matrix:
include:
- platform: linux/amd64
runner: ubuntu-latest
arch: amd64
- platform: linux/arm64
runner: ubuntu-24.04-arm
arch: arm64
timeout-minutes: 30
permissions:
contents: read
@@ -61,24 +70,25 @@ jobs:
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1
- name: Build and push MCP container (latest)
if: github.event_name == 'push'
- name: Build and push MCP container for ${{ matrix.arch }}
if: github.event_name == 'push' || github.event_name == 'release'
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0
with:
context: ${{ env.WORKING_DIRECTORY }}
push: true
platforms: ${{ matrix.platform }}
tags: |
${{ env.PROWLERCLOUD_DOCKERHUB_REPOSITORY }}/${{ env.PROWLERCLOUD_DOCKERHUB_IMAGE }}:${{ env.LATEST_TAG }}
${{ env.PROWLERCLOUD_DOCKERHUB_REPOSITORY }}/${{ env.PROWLERCLOUD_DOCKERHUB_IMAGE }}:${{ needs.setup.outputs.short-sha }}
${{ env.PROWLERCLOUD_DOCKERHUB_REPOSITORY }}/${{ env.PROWLERCLOUD_DOCKERHUB_IMAGE }}:${{ needs.setup.outputs.short-sha }}-${{ matrix.arch }}
labels: |
org.opencontainers.image.title=Prowler MCP Server
org.opencontainers.image.description=Model Context Protocol server for Prowler
org.opencontainers.image.vendor=ProwlerPro, Inc.
org.opencontainers.image.source=https://github.com/${{ github.repository }}
org.opencontainers.image.revision=${{ github.sha }}
org.opencontainers.image.created=${{ github.event.head_commit.timestamp }}
cache-from: type=gha
cache-to: type=gha,mode=max
org.opencontainers.image.created=${{ github.event_name == 'release' && github.event.release.published_at || github.event.head_commit.timestamp }}
${{ github.event_name == 'release' && format('org.opencontainers.image.version={0}', env.RELEASE_TAG) || '' }}
cache-from: type=gha,scope=${{ matrix.arch }}
cache-to: type=gha,mode=max,scope=${{ matrix.arch }}
- name: Notify container push started
if: github.event_name == 'release'
@@ -94,27 +104,6 @@ jobs:
slack-bot-token: ${{ secrets.SLACK_BOT_TOKEN }}
payload-file-path: "./.github/scripts/slack-messages/container-release-started.json"
- name: Build and push MCP container (release)
if: github.event_name == 'release'
id: container-push
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0
with:
context: ${{ env.WORKING_DIRECTORY }}
push: true
tags: |
${{ env.PROWLERCLOUD_DOCKERHUB_REPOSITORY }}/${{ env.PROWLERCLOUD_DOCKERHUB_IMAGE }}:${{ env.RELEASE_TAG }}
${{ env.PROWLERCLOUD_DOCKERHUB_REPOSITORY }}/${{ env.PROWLERCLOUD_DOCKERHUB_IMAGE }}:${{ env.STABLE_TAG }}
labels: |
org.opencontainers.image.title=Prowler MCP Server
org.opencontainers.image.description=Model Context Protocol server for Prowler
org.opencontainers.image.vendor=ProwlerPro, Inc.
org.opencontainers.image.version=${{ env.RELEASE_TAG }}
org.opencontainers.image.source=https://github.com/${{ github.repository }}
org.opencontainers.image.revision=${{ github.sha }}
org.opencontainers.image.created=${{ github.event.release.published_at }}
cache-from: type=gha
cache-to: type=gha,mode=max
- name: Notify container push completed
if: github.event_name == 'release' && always()
uses: ./.github/actions/slack-notification
@@ -130,6 +119,52 @@ jobs:
payload-file-path: "./.github/scripts/slack-messages/container-release-completed.json"
step-outcome: ${{ steps.container-push.outcome }}
# Create and push multi-architecture manifest
create-manifest:
needs: [setup, container-build-push]
if: github.event_name == 'push' || github.event_name == 'release'
runs-on: ubuntu-latest
steps:
- name: Login to DockerHub
uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1
- name: Create and push manifests for push event
if: github.event_name == 'push'
run: |
docker buildx imagetools create \
-t ${{ env.PROWLERCLOUD_DOCKERHUB_REPOSITORY }}/${{ env.PROWLERCLOUD_DOCKERHUB_IMAGE }}:${{ env.LATEST_TAG }} \
-t ${{ env.PROWLERCLOUD_DOCKERHUB_REPOSITORY }}/${{ env.PROWLERCLOUD_DOCKERHUB_IMAGE }}:${{ needs.setup.outputs.short-sha }} \
${{ env.PROWLERCLOUD_DOCKERHUB_REPOSITORY }}/${{ env.PROWLERCLOUD_DOCKERHUB_IMAGE }}:${{ needs.setup.outputs.short-sha }}-amd64 \
${{ env.PROWLERCLOUD_DOCKERHUB_REPOSITORY }}/${{ env.PROWLERCLOUD_DOCKERHUB_IMAGE }}:${{ needs.setup.outputs.short-sha }}-arm64
- name: Create and push manifests for release event
if: github.event_name == 'release'
run: |
docker buildx imagetools create \
-t ${{ env.PROWLERCLOUD_DOCKERHUB_REPOSITORY }}/${{ env.PROWLERCLOUD_DOCKERHUB_IMAGE }}:${{ env.RELEASE_TAG }} \
-t ${{ env.PROWLERCLOUD_DOCKERHUB_REPOSITORY }}/${{ env.PROWLERCLOUD_DOCKERHUB_IMAGE }}:${{ env.STABLE_TAG }} \
${{ env.PROWLERCLOUD_DOCKERHUB_REPOSITORY }}/${{ env.PROWLERCLOUD_DOCKERHUB_IMAGE }}:${{ needs.setup.outputs.short-sha }}-amd64 \
${{ env.PROWLERCLOUD_DOCKERHUB_REPOSITORY }}/${{ env.PROWLERCLOUD_DOCKERHUB_IMAGE }}:${{ needs.setup.outputs.short-sha }}-arm64
- name: Install regctl
if: always()
uses: regclient/actions/regctl-installer@main
- name: Cleanup intermediate architecture tags
if: always()
run: |
echo "Cleaning up intermediate tags..."
regctl tag delete "${{ env.PROWLERCLOUD_DOCKERHUB_REPOSITORY }}/${{ env.PROWLERCLOUD_DOCKERHUB_IMAGE }}:${{ needs.setup.outputs.short-sha }}-amd64" || true
regctl tag delete "${{ env.PROWLERCLOUD_DOCKERHUB_REPOSITORY }}/${{ env.PROWLERCLOUD_DOCKERHUB_IMAGE }}:${{ needs.setup.outputs.short-sha }}-arm64" || true
echo "Cleanup completed"
trigger-deployment:
if: github.event_name == 'push'
needs: [setup, container-build-push]
+76 -27
View File
@@ -46,7 +46,16 @@ env:
jobs:
container-build-push:
if: github.repository == 'prowler-cloud/prowler'
runs-on: ubuntu-latest
runs-on: ${{ matrix.runner }}
strategy:
matrix:
include:
- platform: linux/amd64
runner: ubuntu-latest
arch: amd64
- platform: linux/arm64
runner: ubuntu-24.04-arm
arch: arm64
timeout-minutes: 45
permissions:
contents: read
@@ -54,6 +63,8 @@ jobs:
outputs:
prowler_version: ${{ steps.get-prowler-version.outputs.prowler_version }}
prowler_version_major: ${{ steps.get-prowler-version.outputs.prowler_version_major }}
latest_tag: ${{ steps.get-prowler-version.outputs.latest_tag }}
stable_tag: ${{ steps.get-prowler-version.outputs.stable_tag }}
env:
POETRY_VIRTUALENVS_CREATE: 'false'
@@ -88,16 +99,22 @@ jobs:
3)
echo "LATEST_TAG=v3-latest" >> "${GITHUB_ENV}"
echo "STABLE_TAG=v3-stable" >> "${GITHUB_ENV}"
echo "latest_tag=v3-latest" >> "${GITHUB_OUTPUT}"
echo "stable_tag=v3-stable" >> "${GITHUB_OUTPUT}"
echo "✓ Prowler v3 detected - tags: v3-latest, v3-stable"
;;
4)
echo "LATEST_TAG=v4-latest" >> "${GITHUB_ENV}"
echo "STABLE_TAG=v4-stable" >> "${GITHUB_ENV}"
echo "latest_tag=v4-latest" >> "${GITHUB_OUTPUT}"
echo "stable_tag=v4-stable" >> "${GITHUB_OUTPUT}"
echo "✓ Prowler v4 detected - tags: v4-latest, v4-stable"
;;
5)
echo "LATEST_TAG=latest" >> "${GITHUB_ENV}"
echo "STABLE_TAG=stable" >> "${GITHUB_ENV}"
echo "latest_tag=latest" >> "${GITHUB_OUTPUT}"
echo "stable_tag=stable" >> "${GITHUB_OUTPUT}"
echo "✓ Prowler v5 detected - tags: latest, stable"
;;
*)
@@ -124,19 +141,18 @@ jobs:
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1
- name: Build and push SDK container (latest)
if: github.event_name == 'push'
- name: Build and push SDK container for ${{ matrix.arch }}
if: github.event_name == 'push' || github.event_name == 'release'
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0
with:
context: .
file: ${{ env.DOCKERFILE_PATH }}
push: true
platforms: ${{ matrix.platform }}
tags: |
${{ secrets.DOCKER_HUB_REPOSITORY }}/${{ env.IMAGE_NAME }}:${{ env.LATEST_TAG }}
${{ secrets.PUBLIC_ECR_REPOSITORY }}/${{ env.IMAGE_NAME }}:${{ env.LATEST_TAG }}
${{ env.PROWLERCLOUD_DOCKERHUB_REPOSITORY }}/${{ env.PROWLERCLOUD_DOCKERHUB_IMAGE }}:${{ env.LATEST_TAG }}
cache-from: type=gha
cache-to: type=gha,mode=max
${{ env.PROWLERCLOUD_DOCKERHUB_REPOSITORY }}/${{ env.PROWLERCLOUD_DOCKERHUB_IMAGE }}:${{ env.LATEST_TAG }}-${{ matrix.arch }}
cache-from: type=gha,scope=${{ matrix.arch }}
cache-to: type=gha,mode=max,scope=${{ matrix.arch }}
- name: Notify container push started
if: github.event_name == 'release'
@@ -151,25 +167,7 @@ jobs:
with:
slack-bot-token: ${{ secrets.SLACK_BOT_TOKEN }}
payload-file-path: "./.github/scripts/slack-messages/container-release-started.json"
- name: Build and push SDK container (release)
if: github.event_name == 'release'
id: container-push
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0
with:
context: .
file: ${{ env.DOCKERFILE_PATH }}
push: true
tags: |
${{ secrets.DOCKER_HUB_REPOSITORY }}/${{ env.IMAGE_NAME }}:${{ env.PROWLER_VERSION }}
${{ secrets.DOCKER_HUB_REPOSITORY }}/${{ env.IMAGE_NAME }}:${{ env.STABLE_TAG }}
${{ secrets.PUBLIC_ECR_REPOSITORY }}/${{ env.IMAGE_NAME }}:${{ env.PROWLER_VERSION }}
${{ secrets.PUBLIC_ECR_REPOSITORY }}/${{ env.IMAGE_NAME }}:${{ env.STABLE_TAG }}
${{ env.PROWLERCLOUD_DOCKERHUB_REPOSITORY }}/${{ env.PROWLERCLOUD_DOCKERHUB_IMAGE }}:${{ env.PROWLER_VERSION }}
${{ env.PROWLERCLOUD_DOCKERHUB_REPOSITORY }}/${{ env.PROWLERCLOUD_DOCKERHUB_IMAGE }}:${{ env.STABLE_TAG }}
cache-from: type=gha
cache-to: type=gha,mode=max
- name: Notify container push completed
if: github.event_name == 'release' && always()
uses: ./.github/actions/slack-notification
@@ -185,6 +183,57 @@ jobs:
payload-file-path: "./.github/scripts/slack-messages/container-release-completed.json"
step-outcome: ${{ steps.container-push.outcome }}
# Create and push multi-architecture manifest
create-manifest:
needs: [container-build-push]
if: github.event_name == 'push' || github.event_name == 'release'
runs-on: ubuntu-latest
steps:
- name: Login to DockerHub
uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1
- name: Create and push manifests for push event
if: github.event_name == 'push'
run: |
docker buildx imagetools create \
-t ${{ env.PROWLERCLOUD_DOCKERHUB_REPOSITORY }}/${{ env.PROWLERCLOUD_DOCKERHUB_IMAGE }}:${{ needs.container-build-push.outputs.latest_tag }} \
-t ${{ secrets.DOCKER_HUB_REPOSITORY }}/${{ env.PROWLERCLOUD_DOCKERHUB_IMAGE }}:${{ needs.container-build-push.outputs.latest_tag }} \
-t ${{ secrets.PUBLIC_ECR_REPOSITORY }}/${{ env.PROWLERCLOUD_DOCKERHUB_IMAGE }}:${{ needs.container-build-push.outputs.latest_tag }} \
${{ env.PROWLERCLOUD_DOCKERHUB_REPOSITORY }}/${{ env.PROWLERCLOUD_DOCKERHUB_IMAGE }}:${{ needs.container-build-push.outputs.latest_tag }}-amd64 \
${{ env.PROWLERCLOUD_DOCKERHUB_REPOSITORY }}/${{ env.PROWLERCLOUD_DOCKERHUB_IMAGE }}:${{ needs.container-build-push.outputs.latest_tag }}-arm64
- name: Create and push manifests for release event
if: github.event_name == 'release'
run: |
docker buildx imagetools create \
-t ${{ secrets.DOCKER_HUB_REPOSITORY }}/${{ env.IMAGE_NAME }}:${{ needs.container-build-push.outputs.prowler_version }} \
-t ${{ secrets.DOCKER_HUB_REPOSITORY }}/${{ env.IMAGE_NAME }}:${{ needs.container-build-push.outputs.stable_tag }} \
-t ${{ secrets.PUBLIC_ECR_REPOSITORY }}/${{ env.IMAGE_NAME }}:${{ needs.container-build-push.outputs.prowler_version }} \
-t ${{ secrets.PUBLIC_ECR_REPOSITORY }}/${{ env.IMAGE_NAME }}:${{ needs.container-build-push.outputs.stable_tag }} \
-t ${{ env.PROWLERCLOUD_DOCKERHUB_REPOSITORY }}/${{ env.PROWLERCLOUD_DOCKERHUB_IMAGE }}:${{ needs.container-build-push.outputs.prowler_version }} \
-t ${{ env.PROWLERCLOUD_DOCKERHUB_REPOSITORY }}/${{ env.PROWLERCLOUD_DOCKERHUB_IMAGE }}:${{ needs.container-build-push.outputs.stable_tag }} \
${{ env.PROWLERCLOUD_DOCKERHUB_REPOSITORY }}/${{ env.PROWLERCLOUD_DOCKERHUB_IMAGE }}:${{ needs.container-build-push.outputs.latest_tag }}-amd64 \
${{ env.PROWLERCLOUD_DOCKERHUB_REPOSITORY }}/${{ env.PROWLERCLOUD_DOCKERHUB_IMAGE }}:${{ needs.container-build-push.outputs.latest_tag }}-arm64
- name: Install regctl
if: always()
uses: regclient/actions/regctl-installer@main
- name: Cleanup intermediate architecture tags
if: always()
run: |
echo "Cleaning up intermediate tags..."
regctl tag delete "${{ env.PROWLERCLOUD_DOCKERHUB_REPOSITORY }}/${{ env.PROWLERCLOUD_DOCKERHUB_IMAGE }}:${{ needs.container-build-push.outputs.latest_tag }}-amd64" || true
regctl tag delete "${{ env.PROWLERCLOUD_DOCKERHUB_REPOSITORY }}/${{ env.PROWLERCLOUD_DOCKERHUB_IMAGE }}:${{ needs.container-build-push.outputs.latest_tag }}-arm64" || true
echo "Cleanup completed"
dispatch-v3-deployment:
if: needs.container-build-push.outputs.prowler_version_major == '3'
needs: container-build-push
+72 -19
View File
@@ -46,7 +46,16 @@ jobs:
container-build-push:
needs: setup
runs-on: ubuntu-latest
runs-on: ${{ matrix.runner }}
strategy:
matrix:
include:
- platform: linux/amd64
runner: ubuntu-latest
arch: amd64
- platform: linux/arm64
runner: ubuntu-24.04-arm
arch: arm64
timeout-minutes: 30
permissions:
contents: read
@@ -65,7 +74,7 @@ jobs:
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1
- name: Build and push UI container (latest)
- name: Build and push UI container for ${{ matrix.arch }}
if: github.event_name == 'push'
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0
with:
@@ -74,8 +83,22 @@ jobs:
NEXT_PUBLIC_PROWLER_RELEASE_VERSION=${{ needs.setup.outputs.short-sha }}
NEXT_PUBLIC_API_BASE_URL=${{ env.NEXT_PUBLIC_API_BASE_URL }}
push: true
platforms: ${{ matrix.platform }}
tags: |
${{ env.PROWLERCLOUD_DOCKERHUB_REPOSITORY }}/${{ env.PROWLERCLOUD_DOCKERHUB_IMAGE }}:${{ needs.setup.outputs.short-sha }}-${{ matrix.arch }}
cache-from: type=gha,scope=${{ matrix.arch }}
cache-to: type=gha,mode=max,scope=${{ matrix.arch }}
- name: Build and push UI container for ${{ matrix.arch }}
if: github.event_name == 'push' || github.event_name == 'release'
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0
with:
context: ${{ env.WORKING_DIRECTORY }}
build-args: |
NEXT_PUBLIC_PROWLER_RELEASE_VERSION=${{ github.event_name == 'release' && format('v{0}', env.RELEASE_TAG) || needs.setup.outputs.short_sha }}
NEXT_PUBLIC_API_BASE_URL=${{ env.NEXT_PUBLIC_API_BASE_URL }}
push: true
tags: |
${{ env.PROWLERCLOUD_DOCKERHUB_REPOSITORY }}/${{ env.PROWLERCLOUD_DOCKERHUB_IMAGE }}:${{ env.LATEST_TAG }}
${{ env.PROWLERCLOUD_DOCKERHUB_REPOSITORY }}/${{ env.PROWLERCLOUD_DOCKERHUB_IMAGE }}:${{ needs.setup.outputs.short-sha }}
cache-from: type=gha
cache-to: type=gha,mode=max
@@ -94,22 +117,6 @@ jobs:
slack-bot-token: ${{ secrets.SLACK_BOT_TOKEN }}
payload-file-path: "./.github/scripts/slack-messages/container-release-started.json"
- name: Build and push UI container (release)
if: github.event_name == 'release'
id: container-push
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0
with:
context: ${{ env.WORKING_DIRECTORY }}
build-args: |
NEXT_PUBLIC_PROWLER_RELEASE_VERSION=v${{ env.RELEASE_TAG }}
NEXT_PUBLIC_API_BASE_URL=${{ env.NEXT_PUBLIC_API_BASE_URL }}
push: true
tags: |
${{ env.PROWLERCLOUD_DOCKERHUB_REPOSITORY }}/${{ env.PROWLERCLOUD_DOCKERHUB_IMAGE }}:${{ env.RELEASE_TAG }}
${{ env.PROWLERCLOUD_DOCKERHUB_REPOSITORY }}/${{ env.PROWLERCLOUD_DOCKERHUB_IMAGE }}:${{ env.STABLE_TAG }}
cache-from: type=gha
cache-to: type=gha,mode=max
- name: Notify container push completed
if: github.event_name == 'release' && always()
uses: ./.github/actions/slack-notification
@@ -125,6 +132,52 @@ jobs:
payload-file-path: "./.github/scripts/slack-messages/container-release-completed.json"
step-outcome: ${{ steps.container-push.outcome }}
# Create and push multi-architecture manifest
create-manifest:
needs: [setup, container-build-push]
if: github.event_name == 'push' || github.event_name == 'release'
runs-on: ubuntu-latest
steps:
- name: Login to DockerHub
uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1
- name: Create and push manifests for push event
if: github.event_name == 'push'
run: |
docker buildx imagetools create \
-t ${{ env.PROWLERCLOUD_DOCKERHUB_REPOSITORY }}/${{ env.PROWLERCLOUD_DOCKERHUB_IMAGE }}:${{ env.LATEST_TAG }} \
-t ${{ env.PROWLERCLOUD_DOCKERHUB_REPOSITORY }}/${{ env.PROWLERCLOUD_DOCKERHUB_IMAGE }}:${{ needs.setup.outputs.short-sha }} \
${{ env.PROWLERCLOUD_DOCKERHUB_REPOSITORY }}/${{ env.PROWLERCLOUD_DOCKERHUB_IMAGE }}:${{ needs.setup.outputs.short-sha }}-amd64 \
${{ env.PROWLERCLOUD_DOCKERHUB_REPOSITORY }}/${{ env.PROWLERCLOUD_DOCKERHUB_IMAGE }}:${{ needs.setup.outputs.short-sha }}-arm64
- name: Create and push manifests for release event
if: github.event_name == 'release'
run: |
docker buildx imagetools create \
-t ${{ env.PROWLERCLOUD_DOCKERHUB_REPOSITORY }}/${{ env.PROWLERCLOUD_DOCKERHUB_IMAGE }}:${{ env.RELEASE_TAG }} \
-t ${{ env.PROWLERCLOUD_DOCKERHUB_REPOSITORY }}/${{ env.PROWLERCLOUD_DOCKERHUB_IMAGE }}:${{ env.STABLE_TAG }} \
${{ env.PROWLERCLOUD_DOCKERHUB_REPOSITORY }}/${{ env.PROWLERCLOUD_DOCKERHUB_IMAGE }}:${{ needs.setup.outputs.short-sha }}-amd64 \
${{ env.PROWLERCLOUD_DOCKERHUB_REPOSITORY }}/${{ env.PROWLERCLOUD_DOCKERHUB_IMAGE }}:${{ needs.setup.outputs.short-sha }}-arm64
- name: Install regctl
if: always()
uses: regclient/actions/regctl-installer@main
- name: Cleanup intermediate architecture tags
if: always()
run: |
echo "Cleaning up intermediate tags..."
regctl tag delete "${{ env.PROWLERCLOUD_DOCKERHUB_REPOSITORY }}/${{ env.PROWLERCLOUD_DOCKERHUB_IMAGE }}:${{ needs.setup.outputs.short-sha }}-amd64" || true
regctl tag delete "${{ env.PROWLERCLOUD_DOCKERHUB_REPOSITORY }}/${{ env.PROWLERCLOUD_DOCKERHUB_IMAGE }}:${{ needs.setup.outputs.short-sha }}-arm64" || true
echo "Cleanup completed"
trigger-deployment:
if: github.event_name == 'push'
needs: [setup, container-build-push]
+69 -4
View File
@@ -45,21 +45,86 @@ pytest_*.xml
.coverage
htmlcov/
# VSCode files
# VSCode files and settings
.vscode/
*.code-workspace
.vscode-test/
# Cursor files
# VSCode extension settings and workspaces
.history/
.ionide/
# MCP Server Settings (various locations)
**/cline_mcp_settings.json
**/mcp_settings.json
**/mcp-config.json
**/mcpServers.json
.mcp/
# AI Coding Assistants - Cursor
.cursorignore
.cursor/
.cursorrules
# RooCode files
# AI Coding Assistants - RooCode
.roo/
.rooignore
.roomodes
# Cline files
# AI Coding Assistants - Cline (formerly Claude Dev)
.cline/
.clineignore
.clinerules
# AI Coding Assistants - Continue
.continue/
continue.json
.continuerc
.continuerc.json
# AI Coding Assistants - GitHub Copilot
.copilot/
.github/copilot/
# AI Coding Assistants - Amazon Q Developer (formerly CodeWhisperer)
.aws/
.codewhisperer/
.amazonq/
.aws-toolkit/
# AI Coding Assistants - Tabnine
.tabnine/
tabnine_config.json
# AI Coding Assistants - Kiro
.kiro/
.kiroignore
kiro.config.json
# AI Coding Assistants - Aider
.aider/
.aider.chat.history.md
.aider.input.history
.aider.tags.cache.v3/
# AI Coding Assistants - Windsurf
.windsurf/
.windsurfignore
# AI Coding Assistants - Replit Agent
.replit
.replitignore
# AI Coding Assistants - Supermaven
.supermaven/
# AI Coding Assistants - Sourcegraph Cody
.cody/
# AI Coding Assistants - General
.ai/
.aiconfig
ai-config.json
# Terraform
.terraform*
+22
View File
@@ -4,6 +4,10 @@ LABEL maintainer="https://github.com/prowler-cloud/prowler"
LABEL org.opencontainers.image.source="https://github.com/prowler-cloud/prowler"
ARG POWERSHELL_VERSION=7.5.0
ENV POWERSHELL_VERSION=${POWERSHELL_VERSION}
ARG TRIVY_VERSION=0.66.0
ENV TRIVY_VERSION=${TRIVY_VERSION}
# hadolint ignore=DL3008
RUN apt-get update && apt-get install -y --no-install-recommends \
@@ -25,6 +29,24 @@ RUN ARCH=$(uname -m) && \
ln -s /opt/microsoft/powershell/7/pwsh /usr/bin/pwsh && \
rm /tmp/powershell.tar.gz
# Install Trivy for IaC scanning
RUN ARCH=$(uname -m) && \
if [ "$ARCH" = "x86_64" ]; then \
TRIVY_ARCH="Linux-64bit" ; \
elif [ "$ARCH" = "aarch64" ]; then \
TRIVY_ARCH="Linux-ARM64" ; \
else \
echo "Unsupported architecture for Trivy: $ARCH" && exit 1 ; \
fi && \
wget --progress=dot:giga "https://github.com/aquasecurity/trivy/releases/download/v${TRIVY_VERSION}/trivy_${TRIVY_VERSION}_${TRIVY_ARCH}.tar.gz" -O /tmp/trivy.tar.gz && \
tar zxf /tmp/trivy.tar.gz -C /tmp && \
mv /tmp/trivy /usr/local/bin/trivy && \
chmod +x /usr/local/bin/trivy && \
rm /tmp/trivy.tar.gz && \
# Create trivy cache directory with proper permissions
mkdir -p /tmp/.cache/trivy && \
chmod 777 /tmp/.cache/trivy
# Add prowler user
RUN addgroup --gid 1000 prowler && \
adduser --uid 1000 --gid 1000 --disabled-password --gecos "" prowler
+3 -3
View File
@@ -82,9 +82,9 @@ prowler dashboard
| Provider | Checks | Services | [Compliance Frameworks](https://docs.prowler.com/projects/prowler-open-source/en/latest/tutorials/compliance/) | [Categories](https://docs.prowler.com/projects/prowler-open-source/en/latest/tutorials/misc/#categories) | Support | Interface |
|---|---|---|---|---|---|---|
| AWS | 576 | 82 | 38 | 10 | Official | UI, API, CLI |
| GCP | 79 | 13 | 12 | 3 | Official | UI, API, CLI |
| Azure | 162 | 19 | 12 | 4 | Official | UI, API, CLI |
| AWS | 576 | 82 | 39 | 10 | Official | UI, API, CLI |
| GCP | 79 | 13 | 13 | 3 | Official | UI, API, CLI |
| Azure | 162 | 19 | 13 | 4 | Official | UI, API, CLI |
| Kubernetes | 83 | 7 | 5 | 7 | Official | UI, API, CLI |
| GitHub | 17 | 2 | 1 | 0 | Official | Stable | UI, API, CLI |
| M365 | 70 | 7 | 3 | 2 | Official | UI, API, CLI |
+19
View File
@@ -14,10 +14,29 @@ All notable changes to the **Prowler API** are documented in this file.
- Support muting findings based on simple rules with custom reason [(#9051)](https://github.com/prowler-cloud/prowler/pull/9051)
- Support C5 compliance framework for the GCP provider [(#9097)](https://github.com/prowler-cloud/prowler/pull/9097)
- Support for Amazon Bedrock and OpenAI compatible providers in Lighthouse AI [(#8957)](https://github.com/prowler-cloud/prowler/pull/8957)
- Tenant-wide ThreatScore overview aggregation and snapshot persistence with backfill support [(#9148)](https://github.com/prowler-cloud/prowler/pull/9148)
- Added `metadata`, `details`, and `partition` attributes to `/resources` endpoint & `details`, and `partition` to `/findings` endpoint [(#9098)](https://github.com/prowler-cloud/prowler/pull/9098)
- Support for MongoDB Atlas provider [(#9167)](https://github.com/prowler-cloud/prowler/pull/9167)
- Enhanced compliance overview endpoint with provider filtering and latest scan aggregation [(#9244)](https://github.com/prowler-cloud/prowler/pull/9244)
### Changed
- Optimized database write queries for scan related tasks [(#9190)](https://github.com/prowler-cloud/prowler/pull/9190)
- Date filters are now optional for `GET /api/v1/overviews/services` endpoint; returns latest scan data by default [(#9248)](https://github.com/prowler-cloud/prowler/pull/9248)
### Fixed
- Scans no longer fail when findings have UIDs exceeding 300 characters; such findings are now skipped with detailed logging [(#9246)](https://github.com/prowler-cloud/prowler/pull/9246)
### Security
- Django updated to the latest 5.1 security release, 5.1.14, due to problems with potential [SQL injection](https://github.com/prowler-cloud/prowler/security/dependabot/113) and [denial-of-service vulnerability](https://github.com/prowler-cloud/prowler/security/dependabot/114) [(#9176)](https://github.com/prowler-cloud/prowler/pull/9176)
---
## [1.14.2] (Prowler 5.13.2)
### Fixed
- Update unique constraint for `Provider` model to exclude soft-deleted entries, resolving duplicate errors when re-deleting providers [(#9054)](https://github.com/prowler-cloud/prowler/pull/9054)
- Remove compliance generation for providers without compliance frameworks [(#9208)](https://github.com/prowler-cloud/prowler/pull/9208)
## [1.14.1] (Prowler 5.13.1)
### Fixed
+5 -5
View File
@@ -1,4 +1,4 @@
# This file is automatically @generated by Poetry 2.2.0 and should not be changed by hand.
# This file is automatically @generated by Poetry 2.1.4 and should not be changed by hand.
[[package]]
name = "about-time"
@@ -1671,14 +1671,14 @@ with-social = ["django-allauth[socialaccount] (>=64.0.0)"]
[[package]]
name = "django"
version = "5.1.13"
version = "5.1.14"
description = "A high-level Python web framework that encourages rapid development and clean, pragmatic design."
optional = false
python-versions = ">=3.10"
groups = ["main", "dev"]
files = [
{file = "django-5.1.13-py3-none-any.whl", hash = "sha256:06f257f79dc4c17f3f9e23b106a4c5ed1335abecbe731e83c598c941d14fbeed"},
{file = "django-5.1.13.tar.gz", hash = "sha256:543ff21679f15e80edfc01fe7ea35f8291b6d4ea589433882913626a7c1cf929"},
{file = "django-5.1.14-py3-none-any.whl", hash = "sha256:2a4b9c20404fd1bf50aaaa5542a19d860594cba1354f688f642feb271b91df27"},
{file = "django-5.1.14.tar.gz", hash = "sha256:b98409fb31fdd6e8c3a6ba2eef3415cc5c0020057b43b21ba7af6eff5f014831"},
]
[package.dependencies]
@@ -6786,4 +6786,4 @@ type = ["pytest-mypy"]
[metadata]
lock-version = "2.1"
python-versions = ">=3.11,<3.13"
content-hash = "3c9164d668d37d6373eb5200bbe768232ead934d9312b9c68046b1df922789f3"
content-hash = "943e2cd6b87229704550d4e140b36509fb9f58896ebb5834b9fbabe28a9ee92f"
+1 -1
View File
@@ -7,7 +7,7 @@ authors = [{name = "Prowler Engineering", email = "engineering@prowler.com"}]
dependencies = [
"celery[pytest] (>=5.4.0,<6.0.0)",
"dj-rest-auth[with_social,jwt] (==7.0.1)",
"django (==5.1.13)",
"django (==5.1.14)",
"django-allauth[saml] (>=65.8.0,<66.0.0)",
"django-celery-beat (>=2.7.0,<3.0.0)",
"django-celery-results (>=2.5.1,<3.0.0)",
+1
View File
@@ -144,6 +144,7 @@ def generate_scan_compliance(
Returns:
None: This function modifies the compliance_overview in place.
"""
for compliance_id in PROWLER_CHECKS[provider_type][check_id]:
for requirement in compliance_overview[compliance_id]["requirements"].values():
if check_id in requirement["checks"]:
+42 -20
View File
@@ -47,6 +47,7 @@ from api.models import (
StatusChoices,
Task,
TenantAPIKey,
ThreatScoreSnapshot,
User,
)
from api.rls import Tenant
@@ -760,6 +761,14 @@ class RoleFilter(FilterSet):
class ComplianceOverviewFilter(FilterSet):
inserted_at = DateFilter(field_name="inserted_at", lookup_expr="date")
scan_id = UUIDFilter(field_name="scan_id")
provider_id = UUIDFilter(field_name="scan__provider__id", lookup_expr="exact")
provider_id__in = UUIDInFilter(field_name="scan__provider__id", lookup_expr="in")
provider_type = ChoiceFilter(
field_name="scan__provider__provider", choices=Provider.ProviderChoices.choices
)
provider_type__in = ChoiceInFilter(
field_name="scan__provider__provider", choices=Provider.ProviderChoices.choices
)
region = CharFilter(field_name="region")
class Meta:
@@ -859,26 +868,6 @@ class ScanSummarySeverityFilter(ScanSummaryFilter):
}
class ServiceOverviewFilter(ScanSummaryFilter):
def is_valid(self):
# Check if at least one of the inserted_at filters is present
inserted_at_filters = [
self.data.get("inserted_at"),
self.data.get("inserted_at__gte"),
self.data.get("inserted_at__lte"),
]
if not any(inserted_at_filters):
raise ValidationError(
{
"inserted_at": [
"At least one of filter[inserted_at], filter[inserted_at__gte], or "
"filter[inserted_at__lte] is required."
]
}
)
return super().is_valid()
class IntegrationFilter(FilterSet):
inserted_at = DateFilter(field_name="inserted_at", lookup_expr="date")
integration_type = ChoiceFilter(choices=Integration.IntegrationChoices.choices)
@@ -998,3 +987,36 @@ class MuteRuleFilter(FilterSet):
"inserted_at": ["gte", "lte"],
"updated_at": ["gte", "lte"],
}
class ThreatScoreSnapshotFilter(FilterSet):
"""
Filter for ThreatScore snapshots.
Allows filtering by scan, provider, compliance_id, and date ranges.
"""
inserted_at = DateFilter(field_name="inserted_at", lookup_expr="date")
scan_id = UUIDFilter(field_name="scan__id", lookup_expr="exact")
scan_id__in = UUIDInFilter(field_name="scan__id", lookup_expr="in")
provider_id = UUIDFilter(field_name="provider__id", lookup_expr="exact")
provider_id__in = UUIDInFilter(field_name="provider__id", lookup_expr="in")
provider_type = ChoiceFilter(
field_name="provider__provider", choices=Provider.ProviderChoices.choices
)
provider_type__in = ChoiceInFilter(
field_name="provider__provider",
choices=Provider.ProviderChoices.choices,
lookup_expr="in",
)
compliance_id = CharFilter(field_name="compliance_id", lookup_expr="exact")
compliance_id__in = CharInFilter(field_name="compliance_id", lookup_expr="in")
class Meta:
model = ThreatScoreSnapshot
fields = {
"scan": ["exact", "in"],
"provider": ["exact", "in"],
"compliance_id": ["exact", "in"],
"inserted_at": ["date", "gte", "lte"],
"overall_score": ["exact", "gte", "lte"],
}
@@ -0,0 +1,24 @@
# Generated by Django 5.1.13 on 2025-11-06 09:20
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("api", "0055_mongodbatlas_provider"),
]
operations = [
migrations.RemoveConstraint(
model_name="provider",
name="unique_provider_uids",
),
migrations.AddConstraint(
model_name="provider",
constraint=models.UniqueConstraint(
condition=models.Q(("is_deleted", False)),
fields=("tenant_id", "provider", "uid"),
name="unique_provider_uids",
),
),
]
@@ -0,0 +1,170 @@
# Generated by Django 5.1.13 on 2025-10-31 09:04
import uuid
import django.db.models.deletion
from django.db import migrations, models
import api.rls
class Migration(migrations.Migration):
dependencies = [
("api", "0056_remove_provider_unique_provider_uids_and_more"),
]
operations = [
migrations.CreateModel(
name="ThreatScoreSnapshot",
fields=[
(
"id",
models.UUIDField(
default=uuid.uuid4,
editable=False,
primary_key=True,
serialize=False,
),
),
("inserted_at", models.DateTimeField(auto_now_add=True)),
(
"compliance_id",
models.CharField(
help_text="Compliance framework ID (e.g., 'prowler_threatscore_aws')",
max_length=100,
),
),
(
"overall_score",
models.DecimalField(
decimal_places=2,
help_text="Overall ThreatScore percentage (0-100)",
max_digits=5,
),
),
(
"score_delta",
models.DecimalField(
blank=True,
decimal_places=2,
help_text="Score change compared to previous snapshot (positive = improvement)",
max_digits=5,
null=True,
),
),
(
"section_scores",
models.JSONField(
blank=True,
default=dict,
help_text="ThreatScore breakdown by section",
),
),
(
"critical_requirements",
models.JSONField(
blank=True,
default=list,
help_text="List of critical failed requirements (risk >= 4)",
),
),
(
"total_requirements",
models.IntegerField(
default=0, help_text="Total number of requirements evaluated"
),
),
(
"passed_requirements",
models.IntegerField(
default=0, help_text="Number of requirements with PASS status"
),
),
(
"failed_requirements",
models.IntegerField(
default=0, help_text="Number of requirements with FAIL status"
),
),
(
"manual_requirements",
models.IntegerField(
default=0, help_text="Number of requirements with MANUAL status"
),
),
(
"total_findings",
models.IntegerField(
default=0,
help_text="Total number of findings across all requirements",
),
),
(
"passed_findings",
models.IntegerField(
default=0, help_text="Number of findings with PASS status"
),
),
(
"failed_findings",
models.IntegerField(
default=0, help_text="Number of findings with FAIL status"
),
),
(
"provider",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="threatscore_snapshots",
related_query_name="threatscore_snapshot",
to="api.provider",
),
),
(
"scan",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="threatscore_snapshots",
related_query_name="threatscore_snapshot",
to="api.scan",
),
),
(
"tenant",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE, to="api.tenant"
),
),
],
options={
"db_table": "threatscore_snapshots",
"abstract": False,
},
),
migrations.AddIndex(
model_name="threatscoresnapshot",
index=models.Index(
fields=["tenant_id", "scan_id"], name="threatscore_snap_t_scan_idx"
),
),
migrations.AddIndex(
model_name="threatscoresnapshot",
index=models.Index(
fields=["tenant_id", "provider_id"], name="threatscore_snap_t_prov_idx"
),
),
migrations.AddIndex(
model_name="threatscoresnapshot",
index=models.Index(
fields=["tenant_id", "inserted_at"], name="threatscore_snap_t_time_idx"
),
),
migrations.AddConstraint(
model_name="threatscoresnapshot",
constraint=api.rls.RowLevelSecurityConstraint(
"tenant_id",
name="rls_on_threatscoresnapshot",
statements=["SELECT", "INSERT", "UPDATE", "DELETE"],
),
),
]
@@ -0,0 +1,25 @@
from django.contrib.postgres.operations import RemoveIndexConcurrently
from django.db import migrations
class Migration(migrations.Migration):
atomic = False
dependencies = [
("api", "0057_threatscoresnapshot"),
]
operations = [
RemoveIndexConcurrently(
model_name="compliancerequirementoverview",
name="cro_tenant_scan_idx",
),
RemoveIndexConcurrently(
model_name="compliancerequirementoverview",
name="cro_scan_comp_idx",
),
RemoveIndexConcurrently(
model_name="compliancerequirementoverview",
name="cro_scan_comp_req_idx",
),
]
@@ -0,0 +1,75 @@
# Generated by Django 5.1.13 on 2025-10-30 15:23
import uuid
import django.db.models.deletion
from django.db import migrations, models
import api.rls
class Migration(migrations.Migration):
dependencies = [
("api", "0058_drop_redundant_compliance_requirement_indexes"),
]
operations = [
migrations.CreateModel(
name="ComplianceOverviewSummary",
fields=[
(
"id",
models.UUIDField(
default=uuid.uuid4,
editable=False,
primary_key=True,
serialize=False,
),
),
("inserted_at", models.DateTimeField(auto_now_add=True)),
("compliance_id", models.TextField()),
("requirements_passed", models.IntegerField(default=0)),
("requirements_failed", models.IntegerField(default=0)),
("requirements_manual", models.IntegerField(default=0)),
("total_requirements", models.IntegerField(default=0)),
(
"scan",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="compliance_summaries",
related_query_name="compliance_summary",
to="api.scan",
),
),
(
"tenant",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE, to="api.tenant"
),
),
],
options={
"db_table": "compliance_overview_summaries",
"abstract": False,
"indexes": [
models.Index(
fields=["tenant_id", "scan_id"], name="cos_tenant_scan_idx"
)
],
"constraints": [
models.UniqueConstraint(
fields=("tenant_id", "scan_id", "compliance_id"),
name="unique_compliance_summary_per_scan",
)
],
},
),
migrations.AddConstraint(
model_name="complianceoverviewsummary",
constraint=api.rls.RowLevelSecurityConstraint(
"tenant_id",
name="rls_on_complianceoverviewsummary",
statements=["SELECT", "INSERT", "UPDATE", "DELETE"],
),
),
]
+187 -20
View File
@@ -425,7 +425,8 @@ class Provider(RowLevelSecurityProtectedModel):
constraints = [
models.UniqueConstraint(
fields=("tenant_id", "provider", "uid", "is_deleted"),
fields=("tenant_id", "provider", "uid"),
condition=Q(is_deleted=False),
name="unique_provider_uids",
),
RowLevelSecurityConstraint(
@@ -1370,35 +1371,70 @@ class ComplianceRequirementOverview(RowLevelSecurityProtectedModel):
),
]
indexes = [
models.Index(fields=["tenant_id", "scan_id"], name="cro_tenant_scan_idx"),
models.Index(
fields=["tenant_id", "scan_id", "compliance_id"],
name="cro_scan_comp_idx",
),
models.Index(
fields=["tenant_id", "scan_id", "compliance_id", "region"],
name="cro_scan_comp_reg_idx",
),
models.Index(
fields=["tenant_id", "scan_id", "compliance_id", "requirement_id"],
name="cro_scan_comp_req_idx",
),
models.Index(
fields=[
"tenant_id",
"scan_id",
"compliance_id",
"requirement_id",
"region",
],
name="cro_scan_comp_req_reg_idx",
),
]
class JSONAPIMeta:
resource_name = "compliance-requirements-overviews"
class ComplianceOverviewSummary(RowLevelSecurityProtectedModel):
"""
Pre-aggregated compliance overview aggregated across ALL regions.
One row per (scan_id, compliance_id) combination.
This table optimizes the common case where users view overall compliance
without filtering by region. For region-specific views, the detailed
ComplianceRequirementOverview table is used instead.
"""
id = models.UUIDField(primary_key=True, default=uuid4, editable=False)
inserted_at = models.DateTimeField(auto_now_add=True, editable=False)
scan = models.ForeignKey(
Scan,
on_delete=models.CASCADE,
related_name="compliance_summaries",
related_query_name="compliance_summary",
)
compliance_id = models.TextField(blank=False)
# Pre-aggregated scores (computed across ALL regions)
requirements_passed = models.IntegerField(default=0)
requirements_failed = models.IntegerField(default=0)
requirements_manual = models.IntegerField(default=0)
total_requirements = models.IntegerField(default=0)
class Meta(RowLevelSecurityProtectedModel.Meta):
db_table = "compliance_overview_summaries"
constraints = [
models.UniqueConstraint(
fields=("tenant_id", "scan_id", "compliance_id"),
name="unique_compliance_summary_per_scan",
),
RowLevelSecurityConstraint(
field="tenant_id",
name="rls_on_%(class)s",
statements=["SELECT", "INSERT", "DELETE"],
),
]
indexes = [
models.Index(
fields=["tenant_id", "scan_id"],
name="cos_tenant_scan_idx",
),
]
class JSONAPIMeta:
resource_name = "compliance-overview-summaries"
class ScanSummary(RowLevelSecurityProtectedModel):
objects = ActiveProviderManager()
all_objects = models.Manager()
@@ -2238,3 +2274,134 @@ class LighthouseProviderModels(RowLevelSecurityProtectedModel):
class JSONAPIMeta:
resource_name = "lighthouse-models"
class ThreatScoreSnapshot(RowLevelSecurityProtectedModel):
"""
Stores historical ThreatScore metrics for a given scan.
Snapshots are created automatically after each ThreatScore report generation.
"""
id = models.UUIDField(primary_key=True, default=uuid4, editable=False)
inserted_at = models.DateTimeField(auto_now_add=True, editable=False)
scan = models.ForeignKey(
Scan,
on_delete=models.CASCADE,
related_name="threatscore_snapshots",
related_query_name="threatscore_snapshot",
)
provider = models.ForeignKey(
Provider,
on_delete=models.CASCADE,
related_name="threatscore_snapshots",
related_query_name="threatscore_snapshot",
)
compliance_id = models.CharField(
max_length=100,
blank=False,
null=False,
help_text="Compliance framework ID (e.g., 'prowler_threatscore_aws')",
)
# Overall ThreatScore metrics
overall_score = models.DecimalField(
max_digits=5,
decimal_places=2,
help_text="Overall ThreatScore percentage (0-100)",
)
# Score improvement/degradation compared to previous snapshot
score_delta = models.DecimalField(
max_digits=5,
decimal_places=2,
null=True,
blank=True,
help_text="Score change compared to previous snapshot (positive = improvement)",
)
# Section breakdown stored as JSON
# Format: {"1. IAM": 85.5, "2. Attack Surface": 92.3, ...}
section_scores = models.JSONField(
default=dict,
blank=True,
help_text="ThreatScore breakdown by section",
)
# Critical requirements metadata stored as JSON
# Format: [{"requirement_id": "...", "risk_level": 5, "weight": 150, ...}, ...]
critical_requirements = models.JSONField(
default=list,
blank=True,
help_text="List of critical failed requirements (risk >= 4)",
)
# Summary statistics
total_requirements = models.IntegerField(
default=0,
help_text="Total number of requirements evaluated",
)
passed_requirements = models.IntegerField(
default=0,
help_text="Number of requirements with PASS status",
)
failed_requirements = models.IntegerField(
default=0,
help_text="Number of requirements with FAIL status",
)
manual_requirements = models.IntegerField(
default=0,
help_text="Number of requirements with MANUAL status",
)
total_findings = models.IntegerField(
default=0,
help_text="Total number of findings across all requirements",
)
passed_findings = models.IntegerField(
default=0,
help_text="Number of findings with PASS status",
)
failed_findings = models.IntegerField(
default=0,
help_text="Number of findings with FAIL status",
)
def __str__(self):
return f"ThreatScore {self.overall_score}% for scan {self.scan_id} ({self.inserted_at})"
class Meta(RowLevelSecurityProtectedModel.Meta):
db_table = "threatscore_snapshots"
constraints = [
RowLevelSecurityConstraint(
field="tenant_id",
name="rls_on_%(class)s",
statements=["SELECT", "INSERT", "UPDATE", "DELETE"],
),
]
indexes = [
models.Index(
fields=["tenant_id", "scan_id"],
name="threatscore_snap_t_scan_idx",
),
models.Index(
fields=["tenant_id", "provider_id"],
name="threatscore_snap_t_prov_idx",
),
models.Index(
fields=["tenant_id", "inserted_at"],
name="threatscore_snap_t_time_idx",
),
]
class JSONAPIMeta:
resource_name = "threatscore-snapshots"
File diff suppressed because it is too large. Load diff
+716 -4
View File
@@ -4,6 +4,7 @@ import json
import os
import tempfile
from datetime import datetime, timedelta, timezone
from decimal import Decimal
from pathlib import Path
from types import SimpleNamespace
from unittest.mock import ANY, MagicMock, Mock, patch
@@ -34,6 +35,8 @@ from rest_framework.response import Response
from api.compliance import get_compliance_frameworks
from api.db_router import MainRouter
from api.models import (
ComplianceOverviewSummary,
ComplianceRequirementOverview,
Finding,
Integration,
Invitation,
@@ -54,8 +57,10 @@ from api.models import (
Scan,
ScanSummary,
StateChoices,
StatusChoices,
Task,
TenantAPIKey,
ThreatScoreSnapshot,
User,
UserRoleRelationship,
)
@@ -1171,6 +1176,161 @@ class TestProviderViewSet:
assert Provider.objects.get().uid == provider_json_payload["uid"]
assert Provider.objects.get().alias == provider_json_payload["alias"]
@pytest.mark.parametrize(
"provider_json_payload",
(
[
{"provider": "aws", "uid": "111111111111", "alias": "test"},
{"provider": "gcp", "uid": "a12322-test54321", "alias": "test"},
{
"provider": "kubernetes",
"uid": "kubernetes-test-123456789",
"alias": "test",
},
{
"provider": "kubernetes",
"uid": "arn:aws:eks:us-east-1:111122223333:cluster/test-cluster-long-name-123456789",
"alias": "EKS",
},
{
"provider": "kubernetes",
"uid": "gke_aaaa-dev_europe-test1_dev-aaaa-test-cluster-long-name-123456789",
"alias": "GKE",
},
{
"provider": "kubernetes",
"uid": "gke_project/cluster-name",
"alias": "GKE",
},
{
"provider": "kubernetes",
"uid": "admin@k8s-demo",
"alias": "test",
},
{
"provider": "azure",
"uid": "8851db6b-42e5-4533-aa9e-30a32d67e875",
"alias": "test",
},
{
"provider": "m365",
"uid": "TestingPro.onmicrosoft.com",
"alias": "test",
},
{
"provider": "m365",
"uid": "subdomain.domain.es",
"alias": "test",
},
{
"provider": "m365",
"uid": "microsoft.net",
"alias": "test",
},
{
"provider": "m365",
"uid": "subdomain1.subdomain2.subdomain3.subdomain4.domain.net",
"alias": "test",
},
{
"provider": "github",
"uid": "test-user",
"alias": "test",
},
{
"provider": "github",
"uid": "test-organization",
"alias": "GitHub Org",
},
{
"provider": "github",
"uid": "prowler-cloud",
"alias": "Prowler",
},
{
"provider": "github",
"uid": "microsoft",
"alias": "Microsoft",
},
{
"provider": "github",
"uid": "a12345678901234567890123456789012345678",
"alias": "Long Username",
},
]
),
)
@patch("api.v1.views.Task.objects.get")
@patch("api.v1.views.delete_provider_task.delay")
def test_providers_soft_delete(
self,
mock_delete_task,
mock_task_get,
authenticated_client,
provider_json_payload,
tasks_fixture,
):
# Mock the Celery task response
prowler_task = tasks_fixture[0]
task_mock = Mock()
task_mock.id = prowler_task.id
mock_delete_task.return_value = task_mock
mock_task_get.return_value = prowler_task
# 1.Create a provider
response = authenticated_client.post(
reverse("provider-list"), data=provider_json_payload, format="json"
)
assert response.status_code == status.HTTP_201_CREATED
assert Provider.objects.count() == 1
provider_id = response.json()["data"]["id"]
# 2. Soft delete the provider using the actual API endpoint
response = authenticated_client.delete(
reverse("provider-detail", kwargs={"pk": provider_id})
)
assert response.status_code == status.HTTP_202_ACCEPTED
assert Provider.objects.count() == 0
assert Provider.all_objects.count() == 1
mock_delete_task.assert_called_once_with(
provider_id=str(provider_id), tenant_id=ANY
)
# 3. Create a provider with the same UID should succeed (since the old one is soft deleted)
response = authenticated_client.post(
reverse("provider-list"), data=provider_json_payload, format="json"
)
assert response.status_code == status.HTTP_201_CREATED
assert Provider.objects.count() == 1
assert Provider.all_objects.count() == 2
provider_id = response.json()["data"]["id"]
# 4. Creating another provider with the same UID should fail (duplicate)
response = authenticated_client.post(
reverse("provider-list"), data=provider_json_payload, format="json"
)
assert response.status_code == status.HTTP_400_BAD_REQUEST
mock_delete_task.reset_mock()
mock_delete_task.return_value = task_mock
# 5. Delete the second provider
response = authenticated_client.delete(
reverse("provider-detail", kwargs={"pk": provider_id})
)
assert response.status_code == status.HTTP_202_ACCEPTED
assert Provider.objects.count() == 0
assert Provider.all_objects.count() == 2
# 6. Creating a provider with the same UID should succeed again
response = authenticated_client.post(
reverse("provider-list"), data=provider_json_payload, format="json"
)
assert response.status_code == status.HTTP_201_CREATED
assert Provider.objects.count() == 1
assert Provider.all_objects.count() == 3
@pytest.mark.parametrize(
"provider_json_payload, error_code, error_pointer",
(
@@ -3385,6 +3545,9 @@ class TestResourceViewSet:
)
assert response.status_code == status.HTTP_200_OK
assert len(response.json()["data"]) == len(resources_fixture)
assert "metadata" in response.json()["data"][0]["attributes"]
assert "details" in response.json()["data"][0]["attributes"]
assert "partition" in response.json()["data"][0]["attributes"]
@pytest.mark.parametrize(
"include_values, expected_resources",
@@ -5657,16 +5820,44 @@ class TestProviderGroupMembershipViewSet:
@pytest.mark.django_db
class TestComplianceOverviewViewSet:
def test_compliance_overview_list_none(self, authenticated_client):
@pytest.fixture(autouse=True)
def mock_backfill_task(self):
with patch("api.v1.views.backfill_compliance_summaries_task.delay") as mock:
yield mock
def test_compliance_overview_list_none(
self,
authenticated_client,
tenants_fixture,
providers_fixture,
mock_backfill_task,
):
tenant = tenants_fixture[0]
provider = providers_fixture[0]
scan = Scan.objects.create(
name="empty-compliance-scan",
provider=provider,
trigger=Scan.TriggerChoices.MANUAL,
state=StateChoices.COMPLETED,
tenant=tenant,
)
response = authenticated_client.get(
reverse("complianceoverview-list"),
{"filter[scan_id]": "8d20ac7d-4cbc-435e-85f4-359be37af821"},
{"filter[scan_id]": str(scan.id)},
)
assert response.status_code == status.HTTP_200_OK
assert len(response.json()["data"]) == 0
mock_backfill_task.assert_called_once()
_, kwargs = mock_backfill_task.call_args
assert kwargs["scan_id"] == str(scan.id)
assert str(kwargs["tenant_id"]) == str(tenant.id)
def test_compliance_overview_list(
self, authenticated_client, compliance_requirements_overviews_fixture
self,
authenticated_client,
compliance_requirements_overviews_fixture,
mock_backfill_task,
):
# List compliance overviews with existing data
requirement_overview1 = compliance_requirements_overviews_fixture[0]
@@ -5696,6 +5887,112 @@ class TestComplianceOverviewViewSet:
assert "requirements_failed" in attributes
assert "requirements_manual" in attributes
assert "total_requirements" in attributes
mock_backfill_task.assert_called_once()
_, kwargs = mock_backfill_task.call_args
assert kwargs["scan_id"] == scan_id
def test_compliance_overview_list_uses_preaggregated_summaries(
self,
authenticated_client,
tenants_fixture,
providers_fixture,
mock_backfill_task,
):
tenant = tenants_fixture[0]
provider = providers_fixture[0]
scan = Scan.objects.create(
name="preaggregated-scan",
provider=provider,
trigger=Scan.TriggerChoices.MANUAL,
state=StateChoices.COMPLETED,
tenant=tenant,
)
ComplianceRequirementOverview.objects.create(
tenant=tenant,
scan=scan,
compliance_id="cis_1.4_aws",
framework="CIS-1.4-AWS",
version="1.4",
description="CIS AWS Foundations Benchmark v1.4.0",
region="eu-west-1",
requirement_id="framework-metadata",
requirement_status=StatusChoices.PASS,
passed_checks=1,
failed_checks=0,
total_checks=1,
)
ComplianceOverviewSummary.objects.create(
tenant=tenant,
scan=scan,
compliance_id="cis_1.4_aws",
requirements_passed=5,
requirements_failed=1,
requirements_manual=2,
total_requirements=8,
)
response = authenticated_client.get(
reverse("complianceoverview-list"),
{"filter[scan_id]": str(scan.id)},
)
assert response.status_code == status.HTTP_200_OK
data = response.json()["data"]
assert len(data) == 1
overview = data[0]
assert overview["id"] == "cis_1.4_aws"
assert overview["attributes"]["requirements_passed"] == 5
assert overview["attributes"]["requirements_failed"] == 1
assert overview["attributes"]["requirements_manual"] == 2
assert overview["attributes"]["total_requirements"] == 8
assert "framework" in overview["attributes"]
assert "version" in overview["attributes"]
mock_backfill_task.assert_not_called()
def test_compliance_overview_region_filter_skips_backfill(
self,
authenticated_client,
compliance_requirements_overviews_fixture,
mock_backfill_task,
):
requirement_overview = compliance_requirements_overviews_fixture[0]
scan_id = str(requirement_overview.scan.id)
response = authenticated_client.get(
reverse("complianceoverview-list"),
{
"filter[scan_id]": scan_id,
"filter[region]": requirement_overview.region,
},
)
assert response.status_code == status.HTTP_200_OK
assert len(response.json()["data"]) >= 1
mock_backfill_task.assert_not_called()
def test_compliance_overview_list_without_scan_id(
self, authenticated_client, compliance_requirements_overviews_fixture
):
# Ensure the endpoint works without passing a scan filter
response = authenticated_client.get(reverse("complianceoverview-list"))
assert response.status_code == status.HTTP_200_OK
data = response.json()["data"]
assert len(data) == 3
# Validate payload structure
first_item = data[0]
assert "id" in first_item
assert "attributes" in first_item
attributes = first_item["attributes"]
assert "framework" in attributes
assert "version" in attributes
assert "requirements_passed" in attributes
assert "requirements_failed" in attributes
assert "requirements_manual" in attributes
assert "total_requirements" in attributes
def test_compliance_overview_metadata(
self, authenticated_client, compliance_requirements_overviews_fixture
@@ -5849,6 +6146,11 @@ class TestComplianceOverviewViewSet:
requirement_overview1 = compliance_requirements_overviews_fixture[0]
scan_id = str(requirement_overview1.scan.id)
# Remove existing compliance data so the view falls back to task checks
scan = requirement_overview1.scan
ComplianceOverviewSummary.objects.filter(scan=scan).delete()
ComplianceRequirementOverview.objects.filter(scan=scan).delete()
# Mock a running task
with patch.object(
ComplianceOverviewViewSet, "get_task_response_if_running"
@@ -5876,6 +6178,11 @@ class TestComplianceOverviewViewSet:
requirement_overview1 = compliance_requirements_overviews_fixture[0]
scan_id = str(requirement_overview1.scan.id)
# Remove existing compliance data so the view falls back to task checks
scan = requirement_overview1.scan
ComplianceOverviewSummary.objects.filter(scan=scan).delete()
ComplianceRequirementOverview.objects.filter(scan=scan).delete()
# Mock a failed task
with patch.object(
ComplianceOverviewViewSet, "get_task_response_if_running"
@@ -5899,6 +6206,8 @@ class TestComplianceOverviewViewSet:
("framework", "framework", 1),
("version", "version", 1),
("region", "region", 1),
("region__in", "region", 1),
("region.in", "region", 1),
],
)
def test_compliance_overview_filters(
@@ -6066,11 +6375,414 @@ class TestOverviewViewSet:
for entry in grouped_data:
assert "findings" not in entry["attributes"]
def _create_scan(self, tenant, provider, name, started_at=None):
scan_started = started_at or datetime.now(timezone.utc) - timedelta(hours=1)
return Scan.objects.create(
tenant=tenant,
provider=provider,
name=name,
trigger=Scan.TriggerChoices.MANUAL,
state=StateChoices.COMPLETED,
started_at=scan_started,
completed_at=scan_started + timedelta(minutes=30),
)
def _create_threatscore_snapshot(
self,
tenant,
scan,
provider,
*,
compliance_id,
overall_score,
score_delta,
section_scores,
critical_requirements,
total_requirements,
passed_requirements,
failed_requirements,
manual_requirements,
total_findings,
passed_findings,
failed_findings,
):
return ThreatScoreSnapshot.objects.create(
tenant=tenant,
scan=scan,
provider=provider,
compliance_id=compliance_id,
overall_score=Decimal(overall_score),
score_delta=Decimal(score_delta) if score_delta is not None else None,
section_scores=section_scores,
critical_requirements=critical_requirements,
total_requirements=total_requirements,
passed_requirements=passed_requirements,
failed_requirements=failed_requirements,
manual_requirements=manual_requirements,
total_findings=total_findings,
passed_findings=passed_findings,
failed_findings=failed_findings,
)
def test_overview_threatscore_returns_weighted_aggregate_snapshot(
self, authenticated_client, tenants_fixture, providers_fixture
):
tenant = tenants_fixture[0]
provider1, provider2, *_ = providers_fixture
scan1 = self._create_scan(tenant, provider1, "agg-scan-one")
scan2 = self._create_scan(tenant, provider2, "agg-scan-two")
snapshot1 = self._create_threatscore_snapshot(
tenant,
scan1,
provider1,
compliance_id="prowler_threatscore_aws",
overall_score="80.00",
score_delta="5.00",
section_scores={"1. IAM": "70.00", "2. Attack Surface": "60.00"},
critical_requirements=[
{
"requirement_id": "req_shared",
"title": "Shared requirement (preferred)",
"section": "1. IAM",
"subsection": "Sub IAM",
"risk_level": 5,
"weight": 150,
"passed_findings": 14,
"total_findings": 20,
"description": "Higher risk duplicate",
},
{
"requirement_id": "req_unique_one",
"title": "Unique provider one",
"section": "2. Attack Surface",
"subsection": "Sub Attack",
"risk_level": 4,
"weight": 90,
"passed_findings": 20,
"total_findings": 30,
"description": "Lower risk",
},
],
total_requirements=120,
passed_requirements=90,
failed_requirements=30,
manual_requirements=0,
total_findings=100,
passed_findings=70,
failed_findings=30,
)
snapshot2 = self._create_threatscore_snapshot(
tenant,
scan2,
provider2,
compliance_id="prowler_threatscore_aws",
overall_score="20.00",
score_delta="-2.00",
section_scores={
"1. IAM": "10.00",
"2. Attack Surface": "40.00",
"3. Logging": "30.00",
},
critical_requirements=[
{
"requirement_id": "req_shared",
"title": "Shared requirement (secondary)",
"section": "1. IAM",
"subsection": "Sub IAM",
"risk_level": 4,
"weight": 120,
"passed_findings": 8,
"total_findings": 12,
"description": "Lower risk duplicate",
},
{
"requirement_id": "req_unique_two",
"title": "Unique provider two",
"section": "3. Logging",
"subsection": "Sub Logging",
"risk_level": 5,
"weight": 110,
"passed_findings": 6,
"total_findings": 10,
"description": "Another critical requirement",
},
],
total_requirements=80,
passed_requirements=30,
failed_requirements=50,
manual_requirements=0,
total_findings=50,
passed_findings=15,
failed_findings=35,
)
older_inserted = datetime(2025, 1, 1, 12, 0, tzinfo=timezone.utc)
newer_inserted = datetime(2025, 1, 2, 12, 0, tzinfo=timezone.utc)
ThreatScoreSnapshot.objects.filter(id=snapshot1.id).update(
inserted_at=older_inserted
)
ThreatScoreSnapshot.objects.filter(id=snapshot2.id).update(
inserted_at=newer_inserted
)
snapshot2.refresh_from_db()
response = authenticated_client.get(reverse("overview-threatscore"))
assert response.status_code == status.HTTP_200_OK
body = response.json()
assert len(body["data"]) == 1
aggregated = body["data"][0]
assert aggregated["id"] == "n/a"
assert aggregated["relationships"]["scan"]["data"] is None
assert aggregated["relationships"]["provider"]["data"] is None
attrs = aggregated["attributes"]
assert Decimal(attrs["overall_score"]) == Decimal("60.00")
assert Decimal(attrs["score_delta"]) == Decimal("2.67")
assert attrs["inserted_at"] == snapshot2.inserted_at.isoformat().replace(
"+00:00", "Z"
)
assert attrs["total_findings"] == 150
assert attrs["passed_findings"] == 85
assert attrs["failed_findings"] == 65
assert attrs["total_requirements"] == 200
assert attrs["passed_requirements"] == 120
assert attrs["failed_requirements"] == 80
assert attrs["manual_requirements"] == 0
assert attrs["section_scores"] == {
"1. IAM": "50.00",
"2. Attack Surface": "53.33",
"3. Logging": "30.00",
}
expected_critical = [
{
"requirement_id": "req_shared",
"title": "Shared requirement (preferred)",
"section": "1. IAM",
"subsection": "Sub IAM",
"risk_level": 5,
"weight": 150,
"passed_findings": 14,
"total_findings": 20,
"description": "Higher risk duplicate",
},
{
"requirement_id": "req_unique_two",
"title": "Unique provider two",
"section": "3. Logging",
"subsection": "Sub Logging",
"risk_level": 5,
"weight": 110,
"passed_findings": 6,
"total_findings": 10,
"description": "Another critical requirement",
},
{
"requirement_id": "req_unique_one",
"title": "Unique provider one",
"section": "2. Attack Surface",
"subsection": "Sub Attack",
"risk_level": 4,
"weight": 90,
"passed_findings": 20,
"total_findings": 30,
"description": "Lower risk",
},
]
assert attrs["critical_requirements"] == expected_critical
def test_overview_threatscore_weight_fallback_to_requirements(
self, authenticated_client, tenants_fixture, providers_fixture
):
tenant = tenants_fixture[0]
provider1, provider2, *_ = providers_fixture
scan1 = self._create_scan(tenant, provider1, "fallback-scan-1")
scan2 = self._create_scan(tenant, provider2, "fallback-scan-2")
self._create_threatscore_snapshot(
tenant,
scan1,
provider1,
compliance_id="prowler_threatscore_aws",
overall_score="90.00",
score_delta="4.00",
section_scores={"1. IAM": "90.00"},
critical_requirements=[],
total_requirements=10,
passed_requirements=8,
failed_requirements=0,
manual_requirements=2,
total_findings=0,
passed_findings=0,
failed_findings=0,
)
self._create_threatscore_snapshot(
tenant,
scan2,
provider2,
compliance_id="prowler_threatscore_aws",
overall_score="50.00",
score_delta="1.00",
section_scores={"1. IAM": "40.00"},
critical_requirements=[],
total_requirements=12,
passed_requirements=5,
failed_requirements=7,
manual_requirements=0,
total_findings=10,
passed_findings=4,
failed_findings=6,
)
response = authenticated_client.get(reverse("overview-threatscore"))
assert response.status_code == status.HTTP_200_OK
aggregate = response.json()["data"][0]["attributes"]
assert Decimal(aggregate["overall_score"]) == Decimal("67.78")
assert Decimal(aggregate["score_delta"]) == Decimal("2.33")
assert aggregate["total_findings"] == 10
assert aggregate["total_requirements"] == 22
assert aggregate["manual_requirements"] == 2
assert aggregate["section_scores"] == {"1. IAM": "62.22"}
def test_overview_threatscore_filter_by_scan_id_returns_snapshot(
self, authenticated_client, tenants_fixture, providers_fixture
):
tenant = tenants_fixture[0]
provider1, *_ = providers_fixture
scan = self._create_scan(tenant, provider1, "filter-scan")
snapshot = self._create_threatscore_snapshot(
tenant,
scan,
provider1,
compliance_id="prowler_threatscore_aws",
overall_score="75.00",
score_delta="3.00",
section_scores={"1. IAM": "70.00"},
critical_requirements=[],
total_requirements=50,
passed_requirements=30,
failed_requirements=20,
manual_requirements=0,
total_findings=25,
passed_findings=15,
failed_findings=10,
)
response = authenticated_client.get(
reverse("overview-threatscore"), {"filter[scan_id]": str(scan.id)}
)
assert response.status_code == status.HTTP_200_OK
body = response.json()
assert len(body["data"]) == 1
assert body["data"][0]["id"] == str(snapshot.id)
assert body["data"][0]["attributes"]["overall_score"] == "75.00"
def test_overview_threatscore_snapshot_id_returns_specific_snapshot(
self, authenticated_client, tenants_fixture, providers_fixture
):
tenant = tenants_fixture[0]
provider1, *_ = providers_fixture
scan = self._create_scan(tenant, provider1, "snapshot-id-scan")
snapshot = self._create_threatscore_snapshot(
tenant,
scan,
provider1,
compliance_id="prowler_threatscore_aws",
overall_score="88.50",
score_delta=None,
section_scores={"1. IAM": "80.00"},
critical_requirements=[],
total_requirements=60,
passed_requirements=45,
failed_requirements=15,
manual_requirements=0,
total_findings=30,
passed_findings=25,
failed_findings=5,
)
response = authenticated_client.get(
reverse("overview-threatscore"), {"snapshot_id": str(snapshot.id)}
)
assert response.status_code == status.HTTP_200_OK
data = response.json()
assert data["data"]["id"] == str(snapshot.id)
assert data["data"]["attributes"]["score_delta"] is None
def test_overview_threatscore_provider_filter_returns_unaggregated_snapshot(
self, authenticated_client, tenants_fixture, providers_fixture
):
tenant = tenants_fixture[0]
provider1, provider2, *_ = providers_fixture
scan1 = self._create_scan(tenant, provider1, "provider-filter-scan-1")
scan2 = self._create_scan(tenant, provider2, "provider-filter-scan-2")
snapshot1 = self._create_threatscore_snapshot(
tenant,
scan1,
provider1,
compliance_id="prowler_threatscore_aws",
overall_score="55.55",
score_delta="1.10",
section_scores={"1. IAM": "50.00"},
critical_requirements=[],
total_requirements=40,
passed_requirements=25,
failed_requirements=15,
manual_requirements=0,
total_findings=12,
passed_findings=7,
failed_findings=5,
)
self._create_threatscore_snapshot(
tenant,
scan2,
provider2,
compliance_id="prowler_threatscore_aws",
overall_score="44.44",
score_delta="0.80",
section_scores={"1. IAM": "40.00"},
critical_requirements=[],
total_requirements=30,
passed_requirements=18,
failed_requirements=12,
manual_requirements=0,
total_findings=10,
passed_findings=6,
failed_findings=4,
)
response = authenticated_client.get(
reverse("overview-threatscore"),
{"filter[provider_id__in]": str(provider1.id)},
)
assert response.status_code == status.HTTP_200_OK
data = response.json()["data"]
assert len(data) == 1
assert data[0]["id"] == str(snapshot1.id)
assert data[0]["attributes"]["overall_score"] == "55.55"
def test_overview_services_list_no_required_filters(
self, authenticated_client, scan_summaries_fixture
):
response = authenticated_client.get(reverse("overview-services"))
assert response.status_code == status.HTTP_400_BAD_REQUEST
assert response.status_code == status.HTTP_200_OK
# Should return services from latest scans
assert len(response.json()["data"]) == 2
def test_overview_services_list(self, authenticated_client, scan_summaries_fixture):
response = authenticated_client.get(
+72
View File
@@ -47,6 +47,7 @@ from api.models import (
StatusChoices,
Task,
TenantAPIKey,
ThreatScoreSnapshot,
User,
UserRoleRelationship,
)
@@ -1166,11 +1167,17 @@ class ResourceSerializer(RLSSerializer):
"findings",
"failed_findings_count",
"url",
"metadata",
"details",
"partition",
]
extra_kwargs = {
"id": {"read_only": True},
"inserted_at": {"read_only": True},
"updated_at": {"read_only": True},
"metadata": {"read_only": True},
"details": {"read_only": True},
"partition": {"read_only": True},
}
included_serializers = {
@@ -1227,11 +1234,15 @@ class ResourceIncludeSerializer(RLSSerializer):
"service",
"type_",
"tags",
"details",
"partition",
]
extra_kwargs = {
"id": {"read_only": True},
"inserted_at": {"read_only": True},
"updated_at": {"read_only": True},
"details": {"read_only": True},
"partition": {"read_only": True},
}
@extend_schema_field(
@@ -3626,3 +3637,64 @@ class MuteRuleUpdateSerializer(BaseWriteSerializer):
):
raise ValidationError("A mute rule with this name already exists.")
return value
# ThreatScore Snapshots
class ThreatScoreSnapshotSerializer(RLSSerializer):
"""
Serializer for ThreatScore snapshots.
Read-only serializer for retrieving historical ThreatScore metrics.
"""
id = serializers.SerializerMethodField()
class Meta:
model = ThreatScoreSnapshot
fields = [
"id",
"inserted_at",
"scan",
"provider",
"compliance_id",
"overall_score",
"score_delta",
"section_scores",
"critical_requirements",
"total_requirements",
"passed_requirements",
"failed_requirements",
"manual_requirements",
"total_findings",
"passed_findings",
"failed_findings",
]
extra_kwargs = {
"id": {"read_only": True},
"inserted_at": {"read_only": True},
"scan": {"read_only": True},
"provider": {"read_only": True},
"compliance_id": {"read_only": True},
"overall_score": {"read_only": True},
"score_delta": {"read_only": True},
"section_scores": {"read_only": True},
"critical_requirements": {"read_only": True},
"total_requirements": {"read_only": True},
"passed_requirements": {"read_only": True},
"failed_requirements": {"read_only": True},
"manual_requirements": {"read_only": True},
"total_findings": {"read_only": True},
"passed_findings": {"read_only": True},
"failed_findings": {"read_only": True},
}
included_serializers = {
"scan": "api.v1.serializers.ScanIncludeSerializer",
"provider": "api.v1.serializers.ProviderIncludeSerializer",
}
def get_id(self, obj):
if getattr(obj, "_aggregated", False):
return "n/a"
return str(obj.id)
+612 -84
View File
@@ -3,7 +3,10 @@ import glob
import json
import logging
import os
from collections import defaultdict
from copy import deepcopy
from datetime import datetime, timedelta, timezone
from decimal import ROUND_HALF_UP, Decimal, InvalidOperation
from urllib.parse import urljoin
import sentry_sdk
@@ -24,9 +27,23 @@ from django.conf import settings as django_settings
from django.contrib.postgres.aggregates import ArrayAgg
from django.contrib.postgres.search import SearchQuery
from django.db import transaction
from django.db.models import Count, F, Prefetch, Q, Subquery, Sum
from django.db.models import (
Case,
Count,
DecimalField,
ExpressionWrapper,
F,
IntegerField,
Max,
Prefetch,
Q,
Subquery,
Sum,
Value,
When,
)
from django.db.models.functions import Coalesce
from django.http import HttpResponse
from django.http import HttpResponse, QueryDict
from django.shortcuts import redirect
from django.urls import reverse
from django.utils.dateparse import parse_date
@@ -58,6 +75,7 @@ from rest_framework_simplejwt.exceptions import InvalidToken, TokenError
from tasks.beat import schedule_provider_scan
from tasks.jobs.export import get_s3_client
from tasks.tasks import (
backfill_compliance_summaries_task,
backfill_scan_resource_summaries_task,
check_integration_connection_task,
check_lighthouse_connection_task,
@@ -101,13 +119,14 @@ from api.filters import (
ScanFilter,
ScanSummaryFilter,
ScanSummarySeverityFilter,
ServiceOverviewFilter,
TaskFilter,
TenantApiKeyFilter,
TenantFilter,
ThreatScoreSnapshotFilter,
UserFilter,
)
from api.models import (
ComplianceOverviewSummary,
ComplianceRequirementOverview,
Finding,
Integration,
@@ -138,6 +157,7 @@ from api.models import (
StateChoices,
Task,
TenantAPIKey,
ThreatScoreSnapshot,
User,
UserRoleRelationship,
)
@@ -218,6 +238,7 @@ from api.v1.serializers import (
TenantApiKeySerializer,
TenantApiKeyUpdateSerializer,
TenantSerializer,
ThreatScoreSnapshotSerializer,
TokenRefreshSerializer,
TokenSerializer,
TokenSocialLoginSerializer,
@@ -3213,15 +3234,50 @@ class RoleProviderGroupRelationshipView(RelationshipView, BaseRLSViewSet):
@extend_schema_view(
list=extend_schema(
tags=["Compliance Overview"],
summary="List compliance overviews for a scan",
description="Retrieve an overview of all the compliance in a given scan.",
summary="List compliance overviews",
description=(
"Retrieve an overview of all compliance frameworks. "
"If scan_id is provided, returns compliance data for that specific scan. "
"If scan_id is omitted, returns compliance data aggregated from the latest completed scan of each provider."
),
parameters=[
OpenApiParameter(
name="filter[scan_id]",
required=True,
required=False,
type=OpenApiTypes.UUID,
location=OpenApiParameter.QUERY,
description="Related scan ID.",
description=(
"Optional scan ID. If provided, returns compliance for that scan. "
"If omitted, returns compliance for the latest completed scan per provider."
),
),
OpenApiParameter(
name="filter[provider_id]",
required=False,
type=OpenApiTypes.UUID,
location=OpenApiParameter.QUERY,
description="Filter by specific provider ID.",
),
OpenApiParameter(
name="filter[provider_id__in]",
required=False,
type={"type": "array", "items": {"type": "string", "format": "uuid"}},
location=OpenApiParameter.QUERY,
description="Filter by multiple provider IDs (comma-separated).",
),
OpenApiParameter(
name="filter[provider_type]",
required=False,
type=OpenApiTypes.STR,
location=OpenApiParameter.QUERY,
description="Filter by provider type (e.g., aws, azure, gcp).",
),
OpenApiParameter(
name="filter[provider_type__in]",
required=False,
type={"type": "array", "items": {"type": "string"}},
location=OpenApiParameter.QUERY,
description="Filter by multiple provider types (comma-separated).",
),
],
responses={
@@ -3378,33 +3434,45 @@ class ComplianceOverviewViewSet(BaseRLSViewSet, TaskManagementMixin):
def retrieve(self, request, *args, **kwargs):
raise MethodNotAllowed(method="GET")
def list(self, request, *args, **kwargs):
scan_id = request.query_params.get("filter[scan_id]")
if not scan_id:
raise ValidationError(
[
{
"detail": "This query parameter is required.",
"status": 400,
"source": {"pointer": "filter[scan_id]"},
"code": "required",
}
]
)
try:
if task := self.get_task_response_if_running(
task_name="scan-compliance-overviews",
task_kwargs={"tenant_id": self.request.tenant_id, "scan_id": scan_id},
raise_on_not_found=False,
):
return task
except TaskFailedException:
return Response(
{"detail": "Task failed to generate compliance overview data."},
status=status.HTTP_500_INTERNAL_SERVER_ERROR,
)
queryset = self.filter_queryset(self.filter_queryset(self.get_queryset()))
def _compliance_summaries_queryset(self, scan_id):
"""Return pre-aggregated summaries constrained by RBAC visibility."""
role = get_role(self.request.user)
unlimited_visibility = getattr(
role, Permissions.UNLIMITED_VISIBILITY.value, False
)
summaries = ComplianceOverviewSummary.objects.filter(
tenant_id=self.request.tenant_id,
scan_id=scan_id,
)
if not unlimited_visibility:
providers = Provider.all_objects.filter(
provider_groups__in=role.provider_groups.all()
).distinct()
summaries = summaries.filter(scan__provider__in=providers)
return summaries
def _get_compliance_template(self, *, provider=None, scan_id=None):
"""Return the compliance template for the given provider or scan."""
if provider is None and scan_id is not None:
scan = Scan.all_objects.select_related("provider").get(pk=scan_id)
provider = scan.provider
if not provider:
return {}
return PROWLER_COMPLIANCE_OVERVIEW_TEMPLATE.get(provider.provider, {})
def _aggregate_compliance_overview(self, queryset, template_metadata=None):
"""
Aggregate requirement rows into compliance overview dictionaries.
Args:
queryset: ComplianceRequirementOverview queryset already filtered.
template_metadata: Optional dict mapping compliance_id -> metadata.
"""
template_metadata = template_metadata or {}
requirement_status_subquery = queryset.values(
"compliance_id", "requirement_id"
).annotate(
@@ -3414,13 +3482,15 @@ class ComplianceOverviewViewSet(BaseRLSViewSet, TaskManagementMixin):
)
compliance_data = {}
framework_info = {}
for item in queryset.values("compliance_id", "framework", "version").distinct():
framework_info[item["compliance_id"]] = {
fallback_metadata = {
item["compliance_id"]: {
"framework": item["framework"],
"version": item["version"],
}
for item in queryset.values(
"compliance_id", "framework", "version"
).distinct()
}
for item in requirement_status_subquery:
compliance_id = item["compliance_id"]
@@ -3432,32 +3502,36 @@ class ComplianceOverviewViewSet(BaseRLSViewSet, TaskManagementMixin):
else:
req_status = "MANUAL"
if compliance_id not in compliance_data:
compliance_data[compliance_id] = {
compliance_status = compliance_data.setdefault(
compliance_id,
{
"total_requirements": 0,
"requirements_passed": 0,
"requirements_failed": 0,
"requirements_manual": 0,
}
},
)
compliance_data[compliance_id]["total_requirements"] += 1
compliance_status["total_requirements"] += 1
if req_status == "PASS":
compliance_data[compliance_id]["requirements_passed"] += 1
compliance_status["requirements_passed"] += 1
elif req_status == "FAIL":
compliance_data[compliance_id]["requirements_failed"] += 1
compliance_status["requirements_failed"] += 1
else:
compliance_data[compliance_id]["requirements_manual"] += 1
compliance_status["requirements_manual"] += 1
response_data = []
for compliance_id, data in compliance_data.items():
framework = framework_info.get(compliance_id, {})
template = template_metadata.get(compliance_id, {})
fallback = fallback_metadata.get(compliance_id, {})
response_data.append(
{
"id": compliance_id,
"compliance_id": compliance_id,
"framework": framework.get("framework", ""),
"version": framework.get("version", ""),
"framework": template.get("framework")
or fallback.get("framework", ""),
"version": template.get("version") or fallback.get("version", ""),
"requirements_passed": data["requirements_passed"],
"requirements_failed": data["requirements_failed"],
"requirements_manual": data["requirements_manual"],
@@ -3466,7 +3540,151 @@ class ComplianceOverviewViewSet(BaseRLSViewSet, TaskManagementMixin):
)
serializer = self.get_serializer(response_data, many=True)
return Response(serializer.data)
return serializer.data
def _task_response_if_running(self, scan_id):
"""Check for an in-progress task only when no compliance data exists."""
try:
return self.get_task_response_if_running(
task_name="scan-compliance-overviews",
task_kwargs={"tenant_id": self.request.tenant_id, "scan_id": scan_id},
raise_on_not_found=False,
)
except TaskFailedException:
return Response(
{"detail": "Task failed to generate compliance overview data."},
status=status.HTTP_500_INTERNAL_SERVER_ERROR,
)
def _list_with_region_filter(self, scan_id, region_filter):
"""
Fall back to detailed ComplianceRequirementOverview query when region filter is applied.
This uses the original aggregation logic across filtered regions.
"""
regions = region_filter.split(",") if "," in region_filter else [region_filter]
queryset = self.filter_queryset(self.get_queryset()).filter(
scan_id=scan_id,
region__in=regions,
)
data = self._aggregate_compliance_overview(queryset)
if data:
return Response(data)
task_response = self._task_response_if_running(scan_id)
if task_response:
return task_response
return Response(data)
def _list_without_region_aggregation(self, scan_id):
"""
Fall back aggregation when compliance summaries don't exist yet.
Aggregates ComplianceRequirementOverview data across ALL regions.
"""
queryset = self.filter_queryset(self.get_queryset()).filter(scan_id=scan_id)
compliance_template = self._get_compliance_template(scan_id=scan_id)
data = self._aggregate_compliance_overview(
queryset, template_metadata=compliance_template
)
if data:
return Response(data)
task_response = self._task_response_if_running(scan_id)
if task_response:
return task_response
return Response(data)
def list(self, request, *args, **kwargs):
scan_id = request.query_params.get("filter[scan_id]")
tenant_id = self.request.tenant_id
if scan_id:
# Specific scan requested - use optimized summaries with region support
region_filter = request.query_params.get(
"filter[region]"
) or request.query_params.get("filter[region__in]")
if region_filter:
# Fall back to detailed query with region filtering
return self._list_with_region_filter(scan_id, region_filter)
summaries = list(self._compliance_summaries_queryset(scan_id))
if not summaries:
# Trigger async backfill for next time
backfill_compliance_summaries_task.delay(
tenant_id=self.request.tenant_id, scan_id=scan_id
)
# Use fallback aggregation for this request
return self._list_without_region_aggregation(scan_id)
# Get compliance template for provider to enrich with framework/version
compliance_template = self._get_compliance_template(scan_id=scan_id)
# Convert to response format with framework/version enrichment
response_data = []
for summary in summaries:
compliance_metadata = compliance_template.get(summary.compliance_id, {})
response_data.append(
{
"id": summary.compliance_id,
"compliance_id": summary.compliance_id,
"framework": compliance_metadata.get("framework", ""),
"version": compliance_metadata.get("version", ""),
"requirements_passed": summary.requirements_passed,
"requirements_failed": summary.requirements_failed,
"requirements_manual": summary.requirements_manual,
"total_requirements": summary.total_requirements,
}
)
serializer = self.get_serializer(response_data, many=True)
return Response(serializer.data)
else:
# No scan_id provided - use latest scans per provider
# First, check if provider filters are present
provider_id = request.query_params.get("filter[provider_id]")
provider_id__in = request.query_params.get("filter[provider_id__in]")
provider_type = request.query_params.get("filter[provider_type]")
provider_type__in = request.query_params.get("filter[provider_type__in]")
scan_filters = {"tenant_id": tenant_id, "state": StateChoices.COMPLETED}
# Apply provider ID filters
if provider_id:
scan_filters["provider_id"] = provider_id
elif provider_id__in:
# Convert comma-separated string to list
provider_ids = [pid.strip() for pid in provider_id__in.split(",")]
scan_filters["provider_id__in"] = provider_ids
# Apply provider type filters
if provider_type:
scan_filters["provider__provider"] = provider_type
elif provider_type__in:
# Convert comma-separated string to list
provider_types = [pt.strip() for pt in provider_type__in.split(",")]
scan_filters["provider__provider__in"] = provider_types
latest_scan_ids = (
Scan.all_objects.filter(**scan_filters)
.order_by("provider_id", "-inserted_at")
.distinct("provider_id")
.values_list("id", flat=True)
)
base_queryset = self.get_queryset()
queryset = self.filter_queryset(
base_queryset.filter(scan_id__in=latest_scan_ids)
)
# Aggregate compliance data across latest scans
compliance_template = self._get_compliance_template()
data = self._aggregate_compliance_overview(
queryset, template_metadata=compliance_template
)
return Response(data)
@action(detail=False, methods=["get"], url_name="metadata")
def metadata(self, request):
@@ -3482,18 +3700,6 @@ class ComplianceOverviewViewSet(BaseRLSViewSet, TaskManagementMixin):
}
]
)
try:
if task := self.get_task_response_if_running(
task_name="scan-compliance-overviews",
task_kwargs={"tenant_id": self.request.tenant_id, "scan_id": scan_id},
raise_on_not_found=False,
):
return task
except TaskFailedException:
return Response(
{"detail": "Task failed to generate compliance overview data."},
status=status.HTTP_500_INTERNAL_SERVER_ERROR,
)
regions = list(
self.get_queryset()
.filter(scan_id=scan_id)
@@ -3503,6 +3709,15 @@ class ComplianceOverviewViewSet(BaseRLSViewSet, TaskManagementMixin):
)
result = {"regions": regions}
if regions:
serializer = self.get_serializer(data=result)
serializer.is_valid(raise_exception=True)
return Response(serializer.data, status=status.HTTP_200_OK)
task_response = self._task_response_if_running(scan_id)
if task_response:
return task_response
serializer = self.get_serializer(data=result)
serializer.is_valid(raise_exception=True)
return Response(serializer.data, status=status.HTTP_200_OK)
@@ -3535,18 +3750,6 @@ class ComplianceOverviewViewSet(BaseRLSViewSet, TaskManagementMixin):
}
]
)
try:
if task := self.get_task_response_if_running(
task_name="scan-compliance-overviews",
task_kwargs={"tenant_id": self.request.tenant_id, "scan_id": scan_id},
raise_on_not_found=False,
):
return task
except TaskFailedException:
return Response(
{"detail": "Task failed to generate compliance overview data."},
status=status.HTTP_500_INTERNAL_SERVER_ERROR,
)
filtered_queryset = self.filter_queryset(self.get_queryset())
all_requirements = filtered_queryset.values(
@@ -3606,6 +3809,13 @@ class ComplianceOverviewViewSet(BaseRLSViewSet, TaskManagementMixin):
requirements_summary, many=True
)
if requirements_summary:
return Response(serializer.data, status=status.HTTP_200_OK)
task_response = self._task_response_if_running(scan_id)
if task_response:
return task_response
return Response(serializer.data, status=status.HTTP_200_OK)
@action(detail=False, methods=["get"], url_name="attributes")
@@ -3624,15 +3834,6 @@ class ComplianceOverviewViewSet(BaseRLSViewSet, TaskManagementMixin):
)
provider_type = None
try:
sample_requirement = (
self.get_queryset().filter(compliance_id=compliance_id).first()
)
if sample_requirement:
provider_type = sample_requirement.scan.provider.provider
except Exception:
pass
# If we couldn't determine from database, try each provider type
if not provider_type:
@@ -3735,8 +3936,7 @@ class ComplianceOverviewViewSet(BaseRLSViewSet, TaskManagementMixin):
summary="Get findings data by service",
description=(
"Retrieve an aggregated summary of findings grouped by service. The response includes the total count "
"of findings for each service, as long as there are at least one finding for that service. At least "
"one of the `inserted_at` filters must be provided."
"of findings for each service, as long as there are at least one finding for that service."
),
filters=True,
),
@@ -3770,6 +3970,8 @@ class OverviewViewSet(BaseRLSViewSet):
return OverviewSeveritySerializer
elif self.action == "services":
return OverviewServiceSerializer
elif self.action == "threatscore":
return ThreatScoreSnapshotSerializer
return super().get_serializer_class()
def get_filterset_class(self):
@@ -3780,7 +3982,7 @@ class OverviewViewSet(BaseRLSViewSet):
elif self.action == "findings_severity":
return ScanSummarySeverityFilter
elif self.action == "services":
return ServiceOverviewFilter
return ScanSummaryFilter
return None
@extend_schema(exclude=True)
@@ -4011,6 +4213,332 @@ class OverviewViewSet(BaseRLSViewSet):
return Response(serializer.data, status=status.HTTP_200_OK)
@extend_schema(
summary="Get ThreatScore snapshots",
description=(
"Retrieve ThreatScore metrics. By default, returns the latest snapshot for each provider. "
"Use snapshot_id to retrieve a specific historical snapshot."
),
tags=["Overviews"],
parameters=[
OpenApiParameter(
name="snapshot_id",
type=OpenApiTypes.UUID,
location=OpenApiParameter.QUERY,
description="Retrieve a specific snapshot by ID. If not provided, returns latest snapshots.",
),
OpenApiParameter(
name="provider_id",
type=OpenApiTypes.UUID,
location=OpenApiParameter.QUERY,
description="Filter by specific provider ID",
),
OpenApiParameter(
name="provider_id__in",
type=OpenApiTypes.STR,
location=OpenApiParameter.QUERY,
description="Filter by multiple provider IDs (comma-separated UUIDs)",
),
OpenApiParameter(
name="provider_type",
type=OpenApiTypes.STR,
location=OpenApiParameter.QUERY,
description="Filter by provider type (aws, azure, gcp, etc.)",
),
OpenApiParameter(
name="provider_type__in",
type=OpenApiTypes.STR,
location=OpenApiParameter.QUERY,
description="Filter by multiple provider types (comma-separated)",
),
],
)
@action(detail=False, methods=["get"], url_name="threatscore")
def threatscore(self, request):
"""
Get ThreatScore snapshots.
Default behavior: Returns the latest snapshot for each provider.
With snapshot_id: Returns the specific snapshot requested.
"""
tenant_id = self.request.tenant_id
snapshot_id = request.query_params.get("snapshot_id")
# Base queryset with RLS
base_queryset = ThreatScoreSnapshot.objects.filter(tenant_id=tenant_id)
# Apply RBAC filtering
if hasattr(self, "allowed_providers"):
base_queryset = base_queryset.filter(provider__in=self.allowed_providers)
# Case 1: Specific snapshot requested
if snapshot_id:
try:
snapshot = base_queryset.get(id=snapshot_id)
serializer = ThreatScoreSnapshotSerializer(
snapshot, context={"request": request}
)
return Response(serializer.data, status=status.HTTP_200_OK)
except ThreatScoreSnapshot.DoesNotExist:
raise NotFound(detail="ThreatScore snapshot not found")
# Case 2: Latest snapshot per provider (default)
# Apply filters manually: this @action is outside the standard list endpoint flow,
# so DRF's filter backends don't execute and we must flatten JSON:API params ourselves.
normalized_params = QueryDict(mutable=True)
for param_key, values in request.query_params.lists():
normalized_key = param_key
if param_key.startswith("filter[") and param_key.endswith("]"):
normalized_key = param_key[7:-1]
if normalized_key == "snapshot_id":
continue
normalized_params.setlist(normalized_key, values)
filterset = ThreatScoreSnapshotFilter(normalized_params, queryset=base_queryset)
filtered_queryset = filterset.qs
# Get distinct provider IDs from filtered queryset
# Pick the latest snapshot per provider using Postgres DISTINCT ON pattern.
# This avoids issuing one query per provider (N+1) when the filtered dataset is large.
latest_snapshot_ids = list(
filtered_queryset.order_by("provider_id", "-inserted_at")
.distinct("provider_id")
.values_list("id", flat=True)
)
latest_snapshot_map = {
snapshot.id: snapshot
for snapshot in filtered_queryset.filter(id__in=latest_snapshot_ids)
}
latest_snapshots = [
latest_snapshot_map[snapshot_id]
for snapshot_id in latest_snapshot_ids
if snapshot_id in latest_snapshot_map
]
if len(latest_snapshots) <= 1:
serializer = ThreatScoreSnapshotSerializer(
latest_snapshots, many=True, context={"request": request}
)
return Response(serializer.data, status=status.HTTP_200_OK)
snapshot_ids = [
snapshot.id for snapshot in latest_snapshots if snapshot and snapshot.id
]
aggregated_snapshot = self._build_threatscore_overview_snapshot(
snapshot_ids, tenant_id
)
serializer = ThreatScoreSnapshotSerializer(
[aggregated_snapshot], many=True, context={"request": request}
)
return Response(serializer.data, status=status.HTTP_200_OK)
def _build_threatscore_overview_snapshot(self, snapshot_ids, tenant_id):
"""
Aggregate the latest snapshots into a single overview snapshot for the tenant.
"""
if not snapshot_ids:
raise ValueError(
"Snapshot id list cannot be empty when aggregating threatscore overview"
)
base_queryset = ThreatScoreSnapshot.objects.filter(
tenant_id=tenant_id, id__in=snapshot_ids
)
annotated_queryset = (
base_queryset.annotate(
active_requirements=ExpressionWrapper(
F("total_requirements") - F("manual_requirements"),
output_field=IntegerField(),
)
)
.annotate(
weight=Case(
When(total_findings__gt=0, then=F("total_findings")),
When(
active_requirements__gt=0,
then=F("active_requirements"),
),
default=Value(1, output_field=IntegerField()),
output_field=IntegerField(),
)
)
.order_by()
)
aggregated_metrics = annotated_queryset.aggregate(
total_requirements=Sum("total_requirements"),
passed_requirements=Sum("passed_requirements"),
failed_requirements=Sum("failed_requirements"),
manual_requirements=Sum("manual_requirements"),
total_findings=Sum("total_findings"),
passed_findings=Sum("passed_findings"),
failed_findings=Sum("failed_findings"),
weighted_overall_sum=Sum(
ExpressionWrapper(
F("overall_score") * F("weight"),
output_field=DecimalField(max_digits=14, decimal_places=4),
)
),
overall_weight=Sum("weight"),
unweighted_overall_sum=Sum("overall_score"),
weighted_delta_sum=Sum(
Case(
When(
score_delta__isnull=False,
then=ExpressionWrapper(
F("score_delta") * F("weight"),
output_field=DecimalField(max_digits=14, decimal_places=4),
),
),
default=Value(
Decimal("0"),
output_field=DecimalField(max_digits=14, decimal_places=4),
),
output_field=DecimalField(max_digits=14, decimal_places=4),
)
),
delta_weight=Sum(
Case(
When(score_delta__isnull=False, then=F("weight")),
default=Value(0, output_field=IntegerField()),
output_field=IntegerField(),
)
),
provider_count=Count("id"),
latest_inserted_at=Max("inserted_at"),
)
total_requirements = aggregated_metrics["total_requirements"] or 0
passed_requirements = aggregated_metrics["passed_requirements"] or 0
failed_requirements = aggregated_metrics["failed_requirements"] or 0
manual_requirements = aggregated_metrics["manual_requirements"] or 0
total_findings = aggregated_metrics["total_findings"] or 0
passed_findings = aggregated_metrics["passed_findings"] or 0
failed_findings = aggregated_metrics["failed_findings"] or 0
weighted_overall_sum = aggregated_metrics["weighted_overall_sum"]
if weighted_overall_sum is None:
weighted_overall_sum = Decimal("0")
unweighted_overall_sum = aggregated_metrics["unweighted_overall_sum"]
if unweighted_overall_sum is None:
unweighted_overall_sum = Decimal("0")
overall_weight = aggregated_metrics["overall_weight"] or 0
provider_count = aggregated_metrics["provider_count"] or 0
weighted_delta_sum = aggregated_metrics["weighted_delta_sum"]
if weighted_delta_sum is None:
weighted_delta_sum = Decimal("0")
delta_weight = aggregated_metrics["delta_weight"] or 0
if overall_weight > 0:
overall_score = (weighted_overall_sum / Decimal(overall_weight)).quantize(
Decimal("0.01"), rounding=ROUND_HALF_UP
)
elif provider_count > 0:
overall_score = (unweighted_overall_sum / Decimal(provider_count)).quantize(
Decimal("0.01"), rounding=ROUND_HALF_UP
)
else:
overall_score = Decimal("0.00")
if delta_weight > 0:
score_delta = (weighted_delta_sum / Decimal(delta_weight)).quantize(
Decimal("0.01"), rounding=ROUND_HALF_UP
)
else:
score_delta = None
section_weighted_sums = defaultdict(lambda: Decimal("0"))
section_weights = defaultdict(lambda: Decimal("0"))
combined_critical_requirements = {}
snapshots_with_weight = list(annotated_queryset)
for snapshot in snapshots_with_weight:
weight_value = getattr(snapshot, "weight", None)
try:
weight_decimal = Decimal(weight_value)
except (InvalidOperation, TypeError):
weight_decimal = Decimal("1")
if weight_decimal <= 0:
weight_decimal = Decimal("1")
section_scores = snapshot.section_scores or {}
for section, score in section_scores.items():
try:
score_decimal = Decimal(str(score))
except (InvalidOperation, TypeError):
continue
section_weighted_sums[section] += score_decimal * weight_decimal
section_weights[section] += weight_decimal
for requirement in snapshot.critical_requirements or []:
key = requirement.get("requirement_id") or requirement.get("title")
if not key:
continue
existing = combined_critical_requirements.get(key)
def requirement_sort_key(item):
return (
item.get("risk_level") or 0,
item.get("weight") or 0,
)
if existing is None or requirement_sort_key(
requirement
) > requirement_sort_key(existing):
combined_critical_requirements[key] = deepcopy(requirement)
aggregated_section_scores = {}
for section, total in section_weighted_sums.items():
weight_total = section_weights[section]
if weight_total > 0:
aggregated_section_scores[section] = str(
(total / weight_total).quantize(
Decimal("0.01"), rounding=ROUND_HALF_UP
)
)
aggregated_section_scores = dict(sorted(aggregated_section_scores.items()))
aggregated_critical_requirements = sorted(
combined_critical_requirements.values(),
key=lambda item: (
item.get("risk_level") or 0,
item.get("weight") or 0,
),
reverse=True,
)
aggregated_snapshot = ThreatScoreSnapshot(
tenant_id=tenant_id,
scan=None,
provider=None,
compliance_id="prowler_threatscore_overview",
overall_score=overall_score,
score_delta=score_delta,
section_scores=aggregated_section_scores,
critical_requirements=aggregated_critical_requirements,
total_requirements=total_requirements,
passed_requirements=passed_requirements,
failed_requirements=failed_requirements,
manual_requirements=manual_requirements,
total_findings=total_findings,
passed_findings=passed_findings,
failed_findings=failed_findings,
)
latest_inserted_at = aggregated_metrics["latest_inserted_at"]
if latest_inserted_at is not None:
aggregated_snapshot.inserted_at = latest_inserted_at
aggregated_snapshot._aggregated = True
return aggregated_snapshot
@extend_schema(tags=["Schedule"])
@extend_schema_view(
+117 -1
View File
@@ -1,5 +1,10 @@
from collections import defaultdict
from api.db_router import READ_REPLICA_ALIAS
from api.db_utils import rls_transaction
from api.models import (
ComplianceOverviewSummary,
ComplianceRequirementOverview,
Resource,
ResourceFindingMapping,
ResourceScanSummary,
@@ -9,7 +14,7 @@ from api.models import (
def backfill_resource_scan_summaries(tenant_id: str, scan_id: str):
with rls_transaction(tenant_id):
with rls_transaction(tenant_id, using=READ_REPLICA_ALIAS):
if ResourceScanSummary.objects.filter(
tenant_id=tenant_id, scan_id=scan_id
).exists():
@@ -59,3 +64,114 @@ def backfill_resource_scan_summaries(tenant_id: str, scan_id: str):
)
return {"status": "backfilled", "inserted": len(summaries)}
def backfill_compliance_summaries(tenant_id: str, scan_id: str):
"""
Backfill ComplianceOverviewSummary records for a completed scan.
This function checks if summary records already exist for the scan.
If not, it aggregates compliance requirement data and creates the summaries.
Args:
tenant_id: Target tenant UUID
scan_id: Scan UUID to backfill
Returns:
dict: Status indicating whether backfill was performed
"""
with rls_transaction(tenant_id):
if ComplianceOverviewSummary.objects.filter(
tenant_id=tenant_id, scan_id=scan_id
).exists():
return {"status": "already backfilled"}
with rls_transaction(tenant_id):
if not Scan.objects.filter(
tenant_id=tenant_id,
id=scan_id,
state__in=(StateChoices.COMPLETED, StateChoices.FAILED),
).exists():
return {"status": "scan is not completed"}
# Fetch all compliance requirement overview rows for this scan
requirement_rows = ComplianceRequirementOverview.objects.filter(
tenant_id=tenant_id, scan_id=scan_id
).values(
"compliance_id",
"requirement_id",
"requirement_status",
)
if not requirement_rows:
return {"status": "no compliance data to backfill"}
# Group by (compliance_id, requirement_id) across regions
requirement_statuses = defaultdict(
lambda: {"fail_count": 0, "pass_count": 0, "total_count": 0}
)
for row in requirement_rows:
compliance_id = row["compliance_id"]
requirement_id = row["requirement_id"]
requirement_status = row["requirement_status"]
# Aggregate requirement status across regions
key = (compliance_id, requirement_id)
requirement_statuses[key]["total_count"] += 1
if requirement_status == "FAIL":
requirement_statuses[key]["fail_count"] += 1
elif requirement_status == "PASS":
requirement_statuses[key]["pass_count"] += 1
# Determine per-requirement status and aggregate to compliance level
compliance_summaries = defaultdict(
lambda: {
"total_requirements": 0,
"requirements_passed": 0,
"requirements_failed": 0,
"requirements_manual": 0,
}
)
for (compliance_id, requirement_id), counts in requirement_statuses.items():
# Apply business rule: any FAIL → requirement fails
if counts["fail_count"] > 0:
req_status = "FAIL"
elif counts["pass_count"] == counts["total_count"]:
req_status = "PASS"
else:
req_status = "MANUAL"
# Aggregate to compliance level
compliance_summaries[compliance_id]["total_requirements"] += 1
if req_status == "PASS":
compliance_summaries[compliance_id]["requirements_passed"] += 1
elif req_status == "FAIL":
compliance_summaries[compliance_id]["requirements_failed"] += 1
else:
compliance_summaries[compliance_id]["requirements_manual"] += 1
# Create summary objects
summary_objects = []
for compliance_id, data in compliance_summaries.items():
summary_objects.append(
ComplianceOverviewSummary(
tenant_id=tenant_id,
scan_id=scan_id,
compliance_id=compliance_id,
requirements_passed=data["requirements_passed"],
requirements_failed=data["requirements_failed"],
requirements_manual=data["requirements_manual"],
total_requirements=data["total_requirements"],
)
)
# Bulk insert summaries
if summary_objects:
ComplianceOverviewSummary.objects.bulk_create(
summary_objects, batch_size=500, ignore_conflicts=True
)
return {"status": "backfilled", "inserted": len(summary_objects)}
+75 -132
View File
@@ -7,7 +7,6 @@ from shutil import rmtree
import matplotlib.pyplot as plt
from celery.utils.log import get_task_logger
from config.django.base import DJANGO_FINDINGS_BATCH_SIZE, DJANGO_TMP_OUTPUT_DIRECTORY
from django.db.models import Count, Q
from reportlab.lib import colors
from reportlab.lib.enums import TA_CENTER
from reportlab.lib.pagesizes import letter
@@ -26,11 +25,22 @@ from reportlab.platypus import (
TableStyle,
)
from tasks.jobs.export import _generate_output_directory, _upload_to_s3
from tasks.jobs.threatscore import compute_threatscore_metrics
from tasks.jobs.threatscore_utils import (
_aggregate_requirement_statistics_from_database,
_calculate_requirements_data_from_statistics,
)
from tasks.utils import batched
from api.db_router import READ_REPLICA_ALIAS
from api.db_utils import rls_transaction
from api.models import Finding, Provider, ScanSummary, StatusChoices
from api.models import (
Finding,
Provider,
ScanSummary,
StatusChoices,
ThreatScoreSnapshot,
)
from api.utils import initialize_prowler_provider
from prowler.lib.check.compliance_models import Compliance
from prowler.lib.outputs.finding import Finding as FindingOutput
@@ -434,56 +444,6 @@ def _add_pdf_footer(canvas_obj: canvas.Canvas, doc: SimpleDocTemplate) -> None:
canvas_obj.drawString(width - text_width - 30, 20, powered_text)
def _aggregate_requirement_statistics_from_database(
tenant_id: str, scan_id: str
) -> dict[str, dict[str, int]]:
"""
Aggregate finding statistics by check_id using database aggregation.
This function uses Django ORM aggregation to calculate pass/fail statistics
entirely in the database, avoiding the need to load findings into memory.
Args:
tenant_id (str): The tenant ID for Row-Level Security context.
scan_id (str): The ID of the scan to retrieve findings for.
Returns:
dict[str, dict[str, int]]: Dictionary mapping check_id to statistics:
- 'passed' (int): Number of passed findings for this check
- 'total' (int): Total number of findings for this check
Example:
{
'aws_iam_user_mfa_enabled': {'passed': 10, 'total': 15},
'aws_s3_bucket_public_access': {'passed': 0, 'total': 5}
}
"""
requirement_statistics_by_check_id = {}
with rls_transaction(tenant_id, using=READ_REPLICA_ALIAS):
# Use database aggregation to calculate stats without loading findings into memory
aggregated_statistics_queryset = (
Finding.all_objects.filter(tenant_id=tenant_id, scan_id=scan_id)
.values("check_id")
.annotate(
total_findings=Count("id"),
passed_findings=Count("id", filter=Q(status=StatusChoices.PASS)),
)
)
for aggregated_stat in aggregated_statistics_queryset:
check_id = aggregated_stat["check_id"]
requirement_statistics_by_check_id[check_id] = {
"passed": aggregated_stat["passed_findings"],
"total": aggregated_stat["total_findings"],
}
logger.info(
f"Aggregated statistics for {len(requirement_statistics_by_check_id)} unique checks"
)
return requirement_statistics_by_check_id
def _load_findings_for_requirement_checks(
tenant_id: str, scan_id: str, check_ids: list[str], prowler_provider
) -> dict[str, list[FindingOutput]]:
@@ -544,84 +504,6 @@ def _load_findings_for_requirement_checks(
return dict(findings_by_check_id)
def _calculate_requirements_data_from_statistics(
compliance_obj, requirement_statistics_by_check_id: dict[str, dict[str, int]]
) -> tuple[dict[str, dict], list[dict]]:
"""
Calculate requirement status and statistics using pre-aggregated database statistics.
This function uses O(n) lookups with pre-aggregated statistics from the database,
avoiding the need to iterate over all findings for each requirement.
Args:
compliance_obj: The compliance framework object containing requirements.
requirement_statistics_by_check_id (dict[str, dict[str, int]]): Pre-aggregated statistics
mapping check_id to {'passed': int, 'total': int} counts.
Returns:
tuple[dict[str, dict], list[dict]]: A tuple containing:
- attributes_by_requirement_id: Dictionary mapping requirement IDs to their attributes.
- requirements_list: List of requirement dictionaries with status and statistics.
"""
attributes_by_requirement_id = {}
requirements_list = []
compliance_framework = getattr(compliance_obj, "Framework", "N/A")
compliance_version = getattr(compliance_obj, "Version", "N/A")
for requirement in compliance_obj.Requirements:
requirement_id = requirement.Id
requirement_description = getattr(requirement, "Description", "")
requirement_checks = getattr(requirement, "Checks", [])
requirement_attributes = getattr(requirement, "Attributes", [])
# Store requirement metadata for later use
attributes_by_requirement_id[requirement_id] = {
"attributes": {
"req_attributes": requirement_attributes,
"checks": requirement_checks,
},
"description": requirement_description,
}
# Calculate aggregated passed and total findings for this requirement
total_passed_findings = 0
total_findings_count = 0
for check_id in requirement_checks:
if check_id in requirement_statistics_by_check_id:
check_statistics = requirement_statistics_by_check_id[check_id]
total_findings_count += check_statistics["total"]
total_passed_findings += check_statistics["passed"]
# Determine overall requirement status based on findings
if total_findings_count > 0:
if total_passed_findings == total_findings_count:
requirement_status = StatusChoices.PASS
else:
# Partial pass or complete fail both count as FAIL
requirement_status = StatusChoices.FAIL
else:
# No findings means manual review required
requirement_status = StatusChoices.MANUAL
requirements_list.append(
{
"id": requirement_id,
"attributes": {
"framework": compliance_framework,
"version": compliance_version,
"status": requirement_status,
"description": requirement_description,
"passed_findings": total_passed_findings,
"total_findings": total_findings_count,
},
}
)
return attributes_by_requirement_id, requirements_list
def generate_threatscore_report(
tenant_id: str,
scan_id: str,
@@ -1262,8 +1144,9 @@ def generate_threatscore_report_job(
2. Checks provider type compatibility
3. Generates the output directory
4. Calls generate_threatscore_report to create the PDF
5. Uploads the PDF to S3
6. Cleans up temporary files
5. Computes and stores ThreatScore metrics snapshot
6. Uploads the PDF to S3
7. Cleans up temporary files
Args:
tenant_id (str): The tenant ID for Row-Level Security context.
@@ -1317,6 +1200,66 @@ def generate_threatscore_report_job(
min_risk_level=4,
)
# Compute and store ThreatScore metrics snapshot
logger.info(f"Computing ThreatScore metrics for scan {scan_id}")
try:
metrics = compute_threatscore_metrics(
tenant_id=tenant_id,
scan_id=scan_id,
provider_id=provider_id,
compliance_id=compliance_id,
min_risk_level=4,
)
# Create snapshot in database
with rls_transaction(tenant_id):
# Get previous snapshot for the same provider to calculate delta
previous_snapshot = (
ThreatScoreSnapshot.objects.filter(
tenant_id=tenant_id,
provider_id=provider_id,
compliance_id=compliance_id,
)
.order_by("-inserted_at")
.first()
)
# Calculate score delta (improvement)
score_delta = None
if previous_snapshot:
score_delta = metrics["overall_score"] - float(
previous_snapshot.overall_score
)
snapshot = ThreatScoreSnapshot.objects.create(
tenant_id=tenant_id,
scan_id=scan_id,
provider_id=provider_id,
compliance_id=compliance_id,
overall_score=metrics["overall_score"],
score_delta=score_delta,
section_scores=metrics["section_scores"],
critical_requirements=metrics["critical_requirements"],
total_requirements=metrics["total_requirements"],
passed_requirements=metrics["passed_requirements"],
failed_requirements=metrics["failed_requirements"],
manual_requirements=metrics["manual_requirements"],
total_findings=metrics["total_findings"],
passed_findings=metrics["passed_findings"],
failed_findings=metrics["failed_findings"],
)
delta_msg = (
f" (delta: {score_delta:+.2f}%)" if score_delta is not None else ""
)
logger.info(
f"ThreatScore snapshot created with ID {snapshot.id} "
f"(score: {snapshot.overall_score}%{delta_msg})"
)
except Exception as e:
# Log error but don't fail the job if snapshot creation fails
logger.error(f"Error creating ThreatScore snapshot: {e}")
upload_uri = _upload_to_s3(
tenant_id,
scan_id,
File diff suppressed because it is too large. Load diff
+214
View File
@@ -0,0 +1,214 @@
from celery.utils.log import get_task_logger
from tasks.jobs.threatscore_utils import (
_aggregate_requirement_statistics_from_database,
_calculate_requirements_data_from_statistics,
)
from api.db_router import READ_REPLICA_ALIAS
from api.db_utils import rls_transaction
from api.models import Provider, StatusChoices
from prowler.lib.check.compliance_models import Compliance
logger = get_task_logger(__name__)
def compute_threatscore_metrics(
tenant_id: str,
scan_id: str,
provider_id: str,
compliance_id: str,
min_risk_level: int = 4,
) -> dict:
"""
Compute ThreatScore metrics for a given scan.
This function calculates all the metrics needed for a ThreatScore snapshot:
- Overall ThreatScore percentage
- Section-by-section scores
- Critical failed requirements (risk >= min_risk_level)
- Summary statistics (requirements and findings counts)
Args:
tenant_id (str): The tenant ID for Row-Level Security context.
scan_id (str): The ID of the scan to analyze.
provider_id (str): The ID of the provider used in the scan.
compliance_id (str): Compliance framework ID (e.g., "prowler_threatscore_aws").
min_risk_level (int): Minimum risk level for critical requirements. Defaults to 4.
Returns:
dict: A dictionary containing:
- overall_score (float): Overall ThreatScore percentage (0-100)
- section_scores (dict): Section name -> score percentage mapping
- critical_requirements (list): List of critical failed requirement dicts
- total_requirements (int): Total number of requirements
- passed_requirements (int): Number of PASS requirements
- failed_requirements (int): Number of FAIL requirements
- manual_requirements (int): Number of MANUAL requirements
- total_findings (int): Total findings count
- passed_findings (int): Passed findings count
- failed_findings (int): Failed findings count
Example:
>>> metrics = compute_threatscore_metrics(
... tenant_id="tenant-123",
... scan_id="scan-456",
... provider_id="provider-789",
... compliance_id="prowler_threatscore_aws"
... )
>>> print(f"Overall ThreatScore: {metrics['overall_score']:.2f}%")
"""
# Get provider and compliance information
with rls_transaction(tenant_id, using=READ_REPLICA_ALIAS):
provider_obj = Provider.objects.get(id=provider_id)
provider_type = provider_obj.provider
frameworks_bulk = Compliance.get_bulk(provider_type)
compliance_obj = frameworks_bulk[compliance_id]
# Aggregate requirement statistics from database
requirement_statistics_by_check_id = (
_aggregate_requirement_statistics_from_database(tenant_id, scan_id)
)
# Calculate requirements data using aggregated statistics
attributes_by_requirement_id, requirements_list = (
_calculate_requirements_data_from_statistics(
compliance_obj, requirement_statistics_by_check_id
)
)
# Initialize metrics
overall_numerator = 0
overall_denominator = 0
overall_has_findings = False
sections_data = {}
total_requirements = len(requirements_list)
passed_requirements = 0
failed_requirements = 0
manual_requirements = 0
total_findings = 0
passed_findings = 0
failed_findings = 0
critical_requirements_list = []
# Process each requirement
for requirement in requirements_list:
requirement_id = requirement["id"]
requirement_status = requirement["attributes"]["status"]
requirement_attributes = attributes_by_requirement_id.get(requirement_id, {})
# Count requirements by status
if requirement_status == StatusChoices.PASS:
passed_requirements += 1
elif requirement_status == StatusChoices.FAIL:
failed_requirements += 1
elif requirement_status == StatusChoices.MANUAL:
manual_requirements += 1
# Get findings data
req_passed_findings = requirement["attributes"].get("passed_findings", 0)
req_total_findings = requirement["attributes"].get("total_findings", 0)
# Accumulate findings counts
total_findings += req_total_findings
passed_findings += req_passed_findings
failed_findings += req_total_findings - req_passed_findings
# Skip requirements with no findings
if req_total_findings == 0:
continue
overall_has_findings = True
# Get requirement metadata
metadata = requirement_attributes.get("attributes", {}).get(
"req_attributes", []
)
if not metadata or len(metadata) == 0:
continue
m = metadata[0]
risk_level = getattr(m, "LevelOfRisk", 0)
weight = getattr(m, "Weight", 0)
section = getattr(m, "Section", "Unknown")
# Calculate ThreatScore components using formula from UI
rate_i = req_passed_findings / req_total_findings
rfac_i = 1 + 0.25 * risk_level
# Update overall score
overall_numerator += rate_i * req_total_findings * weight * rfac_i
overall_denominator += req_total_findings * weight * rfac_i
# Update section scores
if section not in sections_data:
sections_data[section] = {
"numerator": 0,
"denominator": 0,
"has_findings": False,
}
sections_data[section]["has_findings"] = True
sections_data[section]["numerator"] += (
rate_i * req_total_findings * weight * rfac_i
)
sections_data[section]["denominator"] += req_total_findings * weight * rfac_i
# Identify critical failed requirements
if requirement_status == StatusChoices.FAIL and risk_level >= min_risk_level:
critical_requirements_list.append(
{
"requirement_id": requirement_id,
"title": getattr(m, "Title", "N/A"),
"section": section,
"subsection": getattr(m, "SubSection", "N/A"),
"risk_level": risk_level,
"weight": weight,
"passed_findings": req_passed_findings,
"total_findings": req_total_findings,
"description": getattr(m, "AttributeDescription", "N/A"),
}
)
# Calculate overall ThreatScore
if not overall_has_findings:
overall_score = 100.0
elif overall_denominator > 0:
overall_score = (overall_numerator / overall_denominator) * 100
else:
overall_score = 0.0
# Calculate section scores
section_scores = {}
for section, data in sections_data.items():
if data["has_findings"] and data["denominator"] > 0:
section_scores[section] = (data["numerator"] / data["denominator"]) * 100
else:
section_scores[section] = 100.0
# Sort critical requirements by risk level (desc) and weight (desc)
critical_requirements_list.sort(
key=lambda x: (x["risk_level"], x["weight"]), reverse=True
)
logger.info(
f"ThreatScore computed: {overall_score:.2f}% "
f"({passed_requirements}/{total_requirements} requirements passed, "
f"{len(critical_requirements_list)} critical failures)"
)
return {
"overall_score": round(overall_score, 2),
"section_scores": {k: round(v, 2) for k, v in section_scores.items()},
"critical_requirements": critical_requirements_list,
"total_requirements": total_requirements,
"passed_requirements": passed_requirements,
"failed_requirements": failed_requirements,
"manual_requirements": manual_requirements,
"total_findings": total_findings,
"passed_findings": passed_findings,
"failed_findings": failed_findings,
}
@@ -0,0 +1,127 @@
from celery.utils.log import get_task_logger
from django.db.models import Count, Q
from api.db_router import READ_REPLICA_ALIAS
from api.db_utils import rls_transaction
from api.models import Finding, StatusChoices
logger = get_task_logger(__name__)
def _aggregate_requirement_statistics_from_database(
tenant_id: str, scan_id: str
) -> dict[str, dict[str, int]]:
"""
Aggregate finding statistics by check_id using database aggregation.
This function uses Django ORM aggregation to calculate pass/fail statistics
entirely in the database, avoiding the need to load findings into memory.
Args:
tenant_id (str): The tenant ID for Row-Level Security context.
scan_id (str): The ID of the scan to retrieve findings for.
Returns:
dict[str, dict[str, int]]: Dictionary mapping check_id to statistics:
- 'passed' (int): Number of passed findings for this check
- 'total' (int): Total number of findings for this check
Example:
{
'aws_iam_user_mfa_enabled': {'passed': 10, 'total': 15},
'aws_s3_bucket_public_access': {'passed': 0, 'total': 5}
}
"""
requirement_statistics_by_check_id = {}
with rls_transaction(tenant_id, using=READ_REPLICA_ALIAS):
aggregated_statistics_queryset = (
Finding.all_objects.filter(tenant_id=tenant_id, scan_id=scan_id)
.values("check_id")
.annotate(
total_findings=Count("id"),
passed_findings=Count("id", filter=Q(status=StatusChoices.PASS)),
)
)
for aggregated_stat in aggregated_statistics_queryset:
check_id = aggregated_stat["check_id"]
requirement_statistics_by_check_id[check_id] = {
"passed": aggregated_stat["passed_findings"],
"total": aggregated_stat["total_findings"],
}
logger.info(
f"Aggregated statistics for {len(requirement_statistics_by_check_id)} unique checks"
)
return requirement_statistics_by_check_id
def _calculate_requirements_data_from_statistics(
compliance_obj, requirement_statistics_by_check_id: dict[str, dict[str, int]]
) -> tuple[dict[str, dict], list[dict]]:
"""
Calculate requirement status and statistics using pre-aggregated database statistics.
Args:
compliance_obj: The compliance framework object containing requirements.
requirement_statistics_by_check_id (dict[str, dict[str, int]]): Pre-aggregated statistics
mapping check_id to {'passed': int, 'total': int} counts.
Returns:
tuple[dict[str, dict], list[dict]]: A tuple containing:
- attributes_by_requirement_id: Dictionary mapping requirement IDs to their attributes.
- requirements_list: List of requirement dictionaries with status and statistics.
"""
attributes_by_requirement_id = {}
requirements_list = []
compliance_framework = getattr(compliance_obj, "Framework", "N/A")
compliance_version = getattr(compliance_obj, "Version", "N/A")
for requirement in compliance_obj.Requirements:
requirement_id = requirement.Id
requirement_description = getattr(requirement, "Description", "")
requirement_checks = getattr(requirement, "Checks", [])
requirement_attributes = getattr(requirement, "Attributes", [])
attributes_by_requirement_id[requirement_id] = {
"attributes": {
"req_attributes": requirement_attributes,
"checks": requirement_checks,
},
"description": requirement_description,
}
total_passed_findings = 0
total_findings_count = 0
for check_id in requirement_checks:
if check_id in requirement_statistics_by_check_id:
check_statistics = requirement_statistics_by_check_id[check_id]
total_findings_count += check_statistics["total"]
total_passed_findings += check_statistics["passed"]
if total_findings_count > 0:
if total_passed_findings == total_findings_count:
requirement_status = StatusChoices.PASS
else:
requirement_status = StatusChoices.FAIL
else:
requirement_status = StatusChoices.MANUAL
requirements_list.append(
{
"id": requirement_id,
"attributes": {
"framework": compliance_framework,
"version": compliance_version,
"status": requirement_status,
"description": requirement_description,
"passed_findings": total_passed_findings,
"total_findings": total_findings_count,
},
}
)
return attributes_by_requirement_id, requirements_list
+19 -1
View File
@@ -8,7 +8,10 @@ from celery.utils.log import get_task_logger
from config.celery import RLSTask
from config.django.base import DJANGO_FINDINGS_BATCH_SIZE, DJANGO_TMP_OUTPUT_DIRECTORY
from django_celery_beat.models import PeriodicTask
from tasks.jobs.backfill import backfill_resource_scan_summaries
from tasks.jobs.backfill import (
backfill_compliance_summaries,
backfill_resource_scan_summaries,
)
from tasks.jobs.connection import (
check_integration_connection,
check_lighthouse_connection,
@@ -494,6 +497,21 @@ def backfill_scan_resource_summaries_task(tenant_id: str, scan_id: str):
return backfill_resource_scan_summaries(tenant_id=tenant_id, scan_id=scan_id)
@shared_task(name="backfill-compliance-summaries", queue="backfill")
def backfill_compliance_summaries_task(tenant_id: str, scan_id: str):
"""
Tries to backfill compliance overview summaries for a completed scan.
This task aggregates compliance requirement data across regions
to create pre-computed summary records for fast compliance overview queries.
Args:
tenant_id (str): The tenant identifier.
scan_id (str): The scan identifier.
"""
return backfill_compliance_summaries(tenant_id=tenant_id, scan_id=scan_id)
@shared_task(base=RLSTask, name="scan-compliance-overviews", queue="compliance")
def create_compliance_requirements_task(tenant_id: str, scan_id: str):
"""
+127 -32
View File
@@ -1,43 +1,53 @@
from uuid import uuid4
import pytest
from tasks.jobs.backfill import backfill_resource_scan_summaries
from tasks.jobs.backfill import (
backfill_compliance_summaries,
backfill_resource_scan_summaries,
)
from api.models import ResourceScanSummary, Scan, StateChoices
from api.models import (
ComplianceOverviewSummary,
ResourceScanSummary,
Scan,
StateChoices,
)
@pytest.fixture(scope="function")
def resource_scan_summary_data(scans_fixture):
scan = scans_fixture[0]
return ResourceScanSummary.objects.create(
tenant_id=scan.tenant_id,
scan_id=scan.id,
resource_id=str(uuid4()),
service="aws",
region="us-east-1",
resource_type="instance",
)
@pytest.fixture(scope="function")
def get_not_completed_scans(providers_fixture):
provider_id = providers_fixture[0].id
tenant_id = providers_fixture[0].tenant_id
scan_1 = Scan.objects.create(
tenant_id=tenant_id,
trigger=Scan.TriggerChoices.MANUAL,
state=StateChoices.EXECUTING,
provider_id=provider_id,
)
scan_2 = Scan.objects.create(
tenant_id=tenant_id,
trigger=Scan.TriggerChoices.MANUAL,
state=StateChoices.AVAILABLE,
provider_id=provider_id,
)
return scan_1, scan_2
@pytest.mark.django_db
class TestBackfillResourceScanSummaries:
@pytest.fixture(scope="function")
def resource_scan_summary_data(self, scans_fixture):
scan = scans_fixture[0]
return ResourceScanSummary.objects.create(
tenant_id=scan.tenant_id,
scan_id=scan.id,
resource_id=str(uuid4()),
service="aws",
region="us-east-1",
resource_type="instance",
)
@pytest.fixture(scope="function")
def get_not_completed_scans(self, providers_fixture):
provider_id = providers_fixture[0].id
tenant_id = providers_fixture[0].tenant_id
scan_1 = Scan.objects.create(
tenant_id=tenant_id,
trigger=Scan.TriggerChoices.MANUAL,
state=StateChoices.EXECUTING,
provider_id=provider_id,
)
scan_2 = Scan.objects.create(
tenant_id=tenant_id,
trigger=Scan.TriggerChoices.MANUAL,
state=StateChoices.AVAILABLE,
provider_id=provider_id,
)
return scan_1, scan_2
def test_already_backfilled(self, resource_scan_summary_data):
tenant_id = resource_scan_summary_data.tenant_id
scan_id = resource_scan_summary_data.scan_id
@@ -77,3 +87,88 @@ class TestBackfillResourceScanSummaries:
assert summary.service == resource.service
assert summary.region == resource.region
assert summary.resource_type == resource.type
def test_no_resources_to_backfill(self, scans_fixture):
scan = scans_fixture[1] # Failed scan with no findings/resources
tenant_id = str(scan.tenant_id)
scan_id = str(scan.id)
result = backfill_resource_scan_summaries(tenant_id, scan_id)
assert result == {"status": "no resources to backfill"}
@pytest.mark.django_db
class TestBackfillComplianceSummaries:
def test_already_backfilled(self, scans_fixture):
scan = scans_fixture[0]
tenant_id = str(scan.tenant_id)
ComplianceOverviewSummary.objects.create(
tenant_id=scan.tenant_id,
scan=scan,
compliance_id="aws_account_security_onboarding_aws",
requirements_passed=1,
requirements_failed=0,
requirements_manual=0,
total_requirements=1,
)
result = backfill_compliance_summaries(tenant_id, str(scan.id))
assert result == {"status": "already backfilled"}
def test_not_completed_scan(self, get_not_completed_scans):
for scan in get_not_completed_scans:
result = backfill_compliance_summaries(str(scan.tenant_id), str(scan.id))
assert result == {"status": "scan is not completed"}
def test_no_compliance_data(self, scans_fixture):
scan = scans_fixture[1] # Failed scan with no compliance rows
result = backfill_compliance_summaries(str(scan.tenant_id), str(scan.id))
assert result == {"status": "no compliance data to backfill"}
def test_backfill_creates_compliance_summaries(
self, tenants_fixture, scans_fixture, compliance_requirements_overviews_fixture
):
tenant = tenants_fixture[0]
scan = scans_fixture[0]
result = backfill_compliance_summaries(str(tenant.id), str(scan.id))
expected = {
"aws_account_security_onboarding_aws": {
"requirements_passed": 1,
"requirements_failed": 1,
"requirements_manual": 1,
"total_requirements": 3,
},
"cis_1.4_aws": {
"requirements_passed": 0,
"requirements_failed": 1,
"requirements_manual": 0,
"total_requirements": 1,
},
"mitre_attack_aws": {
"requirements_passed": 0,
"requirements_failed": 1,
"requirements_manual": 0,
"total_requirements": 1,
},
}
assert result == {"status": "backfilled", "inserted": len(expected)}
summaries = ComplianceOverviewSummary.objects.filter(
tenant_id=str(tenant.id), scan_id=str(scan.id)
)
assert summaries.count() == len(expected)
for summary in summaries:
assert summary.compliance_id in expected
expected_counts = expected[summary.compliance_id]
assert summary.requirements_passed == expected_counts["requirements_passed"]
assert summary.requirements_failed == expected_counts["requirements_failed"]
assert summary.requirements_manual == expected_counts["requirements_manual"]
assert summary.total_requirements == expected_counts["total_requirements"]
+151 -6
View File
@@ -1,19 +1,25 @@
import uuid
from datetime import timedelta
from decimal import Decimal
from pathlib import Path
from unittest.mock import MagicMock, patch
import matplotlib
import pytest
from django.utils import timezone
from freezegun import freeze_time
from tasks.jobs.report import (
_aggregate_requirement_statistics_from_database,
_calculate_requirements_data_from_statistics,
_load_findings_for_requirement_checks,
generate_threatscore_report,
generate_threatscore_report_job,
)
from tasks.jobs.threatscore_utils import (
_aggregate_requirement_statistics_from_database,
_calculate_requirements_data_from_statistics,
)
from tasks.tasks import generate_threatscore_report_task
from api.models import Finding, StatusChoices
from api.models import Finding, Scan, StateChoices, StatusChoices, ThreatScoreSnapshot
from prowler.lib.check.models import Severity
matplotlib.use("Agg") # Use non-interactive backend for tests
@@ -39,6 +45,7 @@ class TestGenerateThreatscoreReport:
assert result == {"upload": False}
mock_filter.assert_called_once_with(scan_id=self.scan_id)
@patch("tasks.jobs.report.ThreatScoreSnapshot.objects.create")
@patch("tasks.jobs.report.rmtree")
@patch("tasks.jobs.report._upload_to_s3")
@patch("tasks.jobs.report.generate_threatscore_report")
@@ -53,6 +60,7 @@ class TestGenerateThreatscoreReport:
mock_generate_report,
mock_upload,
mock_rmtree,
mock_snapshot_create,
):
mock_scan_summary_filter.return_value.exists.return_value = True
@@ -95,8 +103,10 @@ class TestGenerateThreatscoreReport:
Path("/tmp/threatscore_path_threatscore_report.pdf").parent,
ignore_errors=True,
)
mock_snapshot_create.assert_called_once()
def test_generate_threatscore_report_fails_upload(self):
@patch("tasks.jobs.report.ThreatScoreSnapshot.objects.create")
def test_generate_threatscore_report_fails_upload(self, mock_snapshot_create):
with (
patch("tasks.jobs.report.ScanSummary.objects.filter") as mock_filter,
patch("tasks.jobs.report.Provider.objects.get") as mock_provider_get,
@@ -125,8 +135,12 @@ class TestGenerateThreatscoreReport:
)
assert result == {"upload": False}
mock_snapshot_create.assert_called_once()
def test_generate_threatscore_report_logs_rmtree_exception(self, caplog):
@patch("tasks.jobs.report.ThreatScoreSnapshot.objects.create")
def test_generate_threatscore_report_logs_rmtree_exception(
self, mock_snapshot_create, caplog
):
with (
patch("tasks.jobs.report.ScanSummary.objects.filter") as mock_filter,
patch("tasks.jobs.report.Provider.objects.get") as mock_provider_get,
@@ -160,8 +174,10 @@ class TestGenerateThreatscoreReport:
provider_id=self.provider_id,
)
assert "Error deleting output files" in caplog.text
mock_snapshot_create.assert_called_once()
def test_generate_threatscore_report_azure_provider(self):
@patch("tasks.jobs.report.ThreatScoreSnapshot.objects.create")
def test_generate_threatscore_report_azure_provider(self, mock_snapshot_create):
with (
patch("tasks.jobs.report.ScanSummary.objects.filter") as mock_filter,
patch("tasks.jobs.report.Provider.objects.get") as mock_provider_get,
@@ -200,6 +216,135 @@ class TestGenerateThreatscoreReport:
only_failed=True,
min_risk_level=4,
)
mock_snapshot_create.assert_called_once()
@patch("tasks.jobs.report.rmtree")
@patch(
"tasks.jobs.report._upload_to_s3",
return_value="s3://bucket/threatscore/threatscore_report.pdf",
)
@patch("tasks.jobs.report.generate_threatscore_report")
@patch("tasks.jobs.report._generate_output_directory")
@patch("tasks.jobs.report.ScanSummary.objects.filter")
@patch("tasks.jobs.report.compute_threatscore_metrics")
@pytest.mark.django_db
@freeze_time("2025-01-10T12:00:00Z")
def test_generate_threatscore_report_persists_snapshot_and_delta(
self,
mock_compute_metrics,
mock_scan_summary_filter,
mock_generate_output_directory,
mock_generate_report,
mock_upload,
mock_rmtree,
tenants_fixture,
providers_fixture,
):
tenant = tenants_fixture[0]
provider = providers_fixture[0]
scan_previous = Scan.objects.create(
tenant=tenant,
provider=provider,
name="previous-threatscore-scan",
trigger=Scan.TriggerChoices.MANUAL,
state=StateChoices.COMPLETED,
started_at=timezone.now() - timedelta(hours=4),
completed_at=timezone.now() - timedelta(hours=3),
)
ThreatScoreSnapshot.objects.create(
tenant=tenant,
scan=scan_previous,
provider=provider,
compliance_id="prowler_threatscore_aws",
overall_score=Decimal("70.00"),
score_delta=None,
section_scores={"1. IAM": "65.00"},
critical_requirements=[],
total_requirements=50,
passed_requirements=35,
failed_requirements=15,
manual_requirements=0,
total_findings=40,
passed_findings=25,
failed_findings=15,
)
scan_current = Scan.objects.create(
tenant=tenant,
provider=provider,
name="current-threatscore-scan",
trigger=Scan.TriggerChoices.MANUAL,
state=StateChoices.COMPLETED,
started_at=timezone.now() - timedelta(hours=2),
completed_at=timezone.now() - timedelta(hours=1),
)
mock_scan_summary_filter.return_value.exists.return_value = True
mock_generate_output_directory.return_value = (
"/tmp/output",
"/tmp/compressed",
"/tmp/threatscore_path",
)
metrics = {
"overall_score": 85.5,
"score_delta": 10.0,
"section_scores": {"1. IAM": 82.3, "2. Attack Surface": 60.0},
"critical_requirements": [
{
"requirement_id": "req_new",
"title": "New high-risk requirement",
"section": "1. IAM",
"subsection": "Root Account",
"risk_level": 5,
"weight": 150,
"passed_findings": 7,
"total_findings": 10,
"description": "Critical requirement description",
}
],
"total_requirements": 140,
"passed_requirements": 100,
"failed_requirements": 40,
"manual_requirements": 0,
"total_findings": 200,
"passed_findings": 150,
"failed_findings": 50,
}
mock_compute_metrics.return_value = metrics
result = generate_threatscore_report_job(
tenant_id=str(tenant.id),
scan_id=str(scan_current.id),
provider_id=str(provider.id),
)
assert result == {"upload": True}
mock_compute_metrics.assert_called_once_with(
tenant_id=str(tenant.id),
scan_id=str(scan_current.id),
provider_id=str(provider.id),
compliance_id="prowler_threatscore_aws",
min_risk_level=4,
)
mock_generate_report.assert_called_once()
mock_upload.assert_called_once()
mock_rmtree.assert_called_once()
snapshots = ThreatScoreSnapshot.objects.filter(
tenant=tenant, provider=provider
).order_by("inserted_at")
assert snapshots.count() == 2
new_snapshot = ThreatScoreSnapshot.objects.get(scan=scan_current)
assert new_snapshot.compliance_id == "prowler_threatscore_aws"
assert Decimal(new_snapshot.overall_score) == Decimal("85.50")
assert Decimal(new_snapshot.score_delta) == Decimal("15.50")
assert new_snapshot.section_scores == metrics["section_scores"]
assert new_snapshot.critical_requirements == metrics["critical_requirements"]
assert new_snapshot.total_requirements == metrics["total_requirements"]
assert new_snapshot.total_findings == metrics["total_findings"]
@pytest.mark.django_db
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,46 @@
import warnings
from dashboard.common_methods import get_section_containers_cis
warnings.filterwarnings("ignore")
def get_table(data):
aux = data[
[
"REQUIREMENTS_ID",
"REQUIREMENTS_DESCRIPTION",
"REQUIREMENTS_ATTRIBUTES_SECTION",
"CHECKID",
"STATUS",
"REGION",
"ACCOUNTID",
"RESOURCEID",
]
].copy()
# Shorten the long FedRAMP KSI descriptions for better display
ksi_short_names = {
"A secure cloud service offering will protect user data, control access, and apply zero trust principles": "Identity and Access Management",
"A secure cloud service offering will use cloud native architecture and design principles to enforce and enhance the Confidentiality, Integrity and Availability of the system": "Cloud Native Architecture",
"A secure cloud service provider will ensure that all system changes are properly documented and configuration baselines are updated accordingly": "Change Management",
"A secure cloud service provider will continuously educate their employees on cybersecurity measures, testing them regularly": "Cybersecurity Education",
"A secure cloud service offering will document, report, and analyze security incidents to ensure regulatory compliance and continuous security improvement": "Incident Reporting",
"A secure cloud service offering will monitor, log, and audit all important events, activity, and changes": "Monitoring, Logging, and Auditing",
"A secure cloud service offering will have intentional, organized, universal guidance for how every information resource, including personnel, is secured": "Policy and Inventory",
"A secure cloud service offering will define, maintain, and test incident response plan(s) and recovery capabilities to ensure minimal service disruption and data loss": "Recovery Planning",
"A secure cloud service offering will follow FedRAMP encryption policies, continuously verify information resource integrity, and restrict access to third-party information resources": "Service Configuration",
"A secure cloud service offering will understand, monitor, and manage supply chain risks from third-party information resources": "Third-Party Information Resources",
}
# Replace long descriptions with short names - use contains for partial matching
if not aux.empty:
for long_desc, short_name in ksi_short_names.items():
mask = aux["REQUIREMENTS_DESCRIPTION"].str.contains(
long_desc, na=False, regex=False
)
aux.loc[mask, "REQUIREMENTS_DESCRIPTION"] = short_name
return get_section_containers_cis(
aux, "REQUIREMENTS_ID", "REQUIREMENTS_ATTRIBUTES_SECTION"
)
@@ -0,0 +1,46 @@
import warnings
from dashboard.common_methods import get_section_containers_cis
warnings.filterwarnings("ignore")
def get_table(data):
aux = data[
[
"REQUIREMENTS_ID",
"REQUIREMENTS_DESCRIPTION",
"REQUIREMENTS_ATTRIBUTES_SECTION",
"CHECKID",
"STATUS",
"REGION",
"ACCOUNTID",
"RESOURCEID",
]
].copy()
# Shorten the long FedRAMP KSI descriptions for better display
ksi_short_names = {
"A secure cloud service offering will protect user data, control access, and apply zero trust principles": "Identity and Access Management",
"A secure cloud service offering will use cloud native architecture and design principles to enforce and enhance the Confidentiality, Integrity and Availability of the system": "Cloud Native Architecture",
"A secure cloud service provider will ensure that all system changes are properly documented and configuration baselines are updated accordingly": "Change Management",
"A secure cloud service provider will continuously educate their employees on cybersecurity measures, testing them regularly": "Cybersecurity Education",
"A secure cloud service offering will document, report, and analyze security incidents to ensure regulatory compliance and continuous security improvement": "Incident Reporting",
"A secure cloud service offering will monitor, log, and audit all important events, activity, and changes": "Monitoring, Logging, and Auditing",
"A secure cloud service offering will have intentional, organized, universal guidance for how every information resource, including personnel, is secured": "Policy and Inventory",
"A secure cloud service offering will define, maintain, and test incident response plan(s) and recovery capabilities to ensure minimal service disruption and data loss": "Recovery Planning",
"A secure cloud service offering will follow FedRAMP encryption policies, continuously verify information resource integrity, and restrict access to third-party information resources": "Service Configuration",
"A secure cloud service offering will understand, monitor, and manage supply chain risks from third-party information resources": "Third-Party Information Resources",
}
# Replace long descriptions with short names - use contains for partial matching
if not aux.empty:
for long_desc, short_name in ksi_short_names.items():
mask = aux["REQUIREMENTS_DESCRIPTION"].str.contains(
long_desc, na=False, regex=False
)
aux.loc[mask, "REQUIREMENTS_DESCRIPTION"] = short_name
return get_section_containers_cis(
aux, "REQUIREMENTS_ID", "REQUIREMENTS_ATTRIBUTES_SECTION"
)
@@ -0,0 +1,46 @@
import warnings
from dashboard.common_methods import get_section_containers_cis
warnings.filterwarnings("ignore")
def get_table(data):
aux = data[
[
"REQUIREMENTS_ID",
"REQUIREMENTS_DESCRIPTION",
"REQUIREMENTS_ATTRIBUTES_SECTION",
"CHECKID",
"STATUS",
"REGION",
"ACCOUNTID",
"RESOURCEID",
]
].copy()
# Shorten the long FedRAMP KSI descriptions for better display
ksi_short_names = {
"A secure cloud service offering will protect user data, control access, and apply zero trust principles": "Identity and Access Management",
"A secure cloud service offering will use cloud native architecture and design principles to enforce and enhance the Confidentiality, Integrity and Availability of the system": "Cloud Native Architecture",
"A secure cloud service provider will ensure that all system changes are properly documented and configuration baselines are updated accordingly": "Change Management",
"A secure cloud service provider will continuously educate their employees on cybersecurity measures, testing them regularly": "Cybersecurity Education",
"A secure cloud service offering will document, report, and analyze security incidents to ensure regulatory compliance and continuous security improvement": "Incident Reporting",
"A secure cloud service offering will monitor, log, and audit all important events, activity, and changes": "Monitoring, Logging, and Auditing",
"A secure cloud service offering will have intentional, organized, universal guidance for how every information resource, including personnel, is secured": "Policy and Inventory",
"A secure cloud service offering will define, maintain, and test incident response plan(s) and recovery capabilities to ensure minimal service disruption and data loss": "Recovery Planning",
"A secure cloud service offering will follow FedRAMP encryption policies, continuously verify information resource integrity, and restrict access to third-party information resources": "Service Configuration",
"A secure cloud service offering will understand, monitor, and manage supply chain risks from third-party information resources": "Third-Party Information Resources",
}
# Replace long descriptions with short names - use contains for partial matching
if not aux.empty:
for long_desc, short_name in ksi_short_names.items():
mask = aux["REQUIREMENTS_DESCRIPTION"].str.contains(
long_desc, na=False, regex=False
)
aux.loc[mask, "REQUIREMENTS_DESCRIPTION"] = short_name
return get_section_containers_cis(
aux, "REQUIREMENTS_ID", "REQUIREMENTS_ATTRIBUTES_SECTION"
)
+24
View File
@@ -0,0 +1,24 @@
import warnings
from dashboard.common_methods import get_section_containers_format3
warnings.filterwarnings("ignore")
def get_table(data):
aux = data[
[
"REQUIREMENTS_ID",
"REQUIREMENTS_ATTRIBUTES_SECTION",
"REQUIREMENTS_DESCRIPTION",
"CHECKID",
"STATUS",
"REGION",
"ACCOUNTID",
"RESOURCEID",
]
].copy()
return get_section_containers_format3(
aux, "REQUIREMENTS_ATTRIBUTES_SECTION", "REQUIREMENTS_ID"
)
+22 -3
View File
@@ -4,7 +4,26 @@ title: 'Contributing to Documentation'
Prowler documentation is built using [Mintlify](https://www.mintlify.com/docs), allowing contributors to easily add or enhance documentation.
## Installation and Setup
## Documentation Structure
The Prowler documentation is organized into several sections. The main ones are:
- **Getting Started**: Provides an overview of the Prowler platform and its different solutions, including Prowler Cloud/App, Prowler CLI, Prowler MCP Server, Prowler Hub, and Prowler Lighthouse AI. This section helps new users understand which Prowler solution best fits their needs and includes product comparisons.
- **Guides**: Contains practical tutorials and how-to guides organized by product (Prowler Cloud/App, CLI) and provider (AWS, Azure, GCP, Kubernetes, Microsoft 365, GitHub, etc.). This section covers authentication, integrations, compliance, and advanced usage scenarios.
- **Developer Guide**: Documentation for contributors looking to extend Prowler functionality. This includes guides on creating providers, services, checks, output formats, integrations, and compliance frameworks. Provider-specific implementation details and testing strategies are also covered here.
- **Troubleshooting**: Common issues, error messages, and their solutions. This section helps users resolve problems encountered during installation, configuration, or execution.
## AI-Driven Documentation
As mentioned in the [Introduction](/developer-guide/introduction#ai-driven-contributions), we have specialized resources to enhance AI-driven development.
This includes the [AGENTS.md](https://github.com/prowler-cloud/prowler/blob/master/docs/AGENTS.md) file that contains the guidelines and style guide for the AI agents in the Prowler documentation.
## Local Development
<Steps>
<Step title="Install Mintlify CLI">
@@ -33,10 +52,10 @@ Prowler documentation is built using [Mintlify](https://www.mintlify.com/docs),
</Step>
<Step title="Submit Changes">
Once documentation updates are complete, submit a pull request for review.
Once documentation updates are complete, [submit a pull request for review](/developer-guide/introduction#sending-the-pull-request).
The Prowler team will assess and merge contributions.
</Step>
</Steps>
Your efforts help improve Prowler documentation—thank you for contributing!
Your efforts help improve Prowler documentation. Thank you for contributing! 🤘
+84 -49
View File
@@ -2,19 +2,70 @@
title: 'Introduction to developing in Prowler'
---
Extending Prowler
Thanks for your interest in contributing to Prowler!
Prowler can be extended in various ways, with common use cases including:
Prowler can be extended in various ways. This guide provides the different ways to contribute and how to get started.
- New security checks
- New compliance frameworks
- New output formats
- New integrations
- New proposed features
## Contributing to Prowler
All the relevant information for these cases is included in this guide.
### Review Current Issues
Check out our [GitHub Issues](https://github.com/prowler-cloud/prowler/issues) page for ideas to contribute.
<Columns cols={2}>
<Card title="Good First Issue" icon="github" href="https://github.com/prowler-cloud/prowler/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20label%3A%22good%20first%20issue%22">
We tag issues as `good first issue` for new contributors. These are typically well-defined and manageable in scope.
</Card>
<Card title="Help Wanted" icon="github" href="https://github.com/prowler-cloud/prowler/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20label%3A%22help%20wanted%22">
We tag issues as `help wanted` for other issues that require more time to complete.
</Card>
</Columns>
## Getting the Code and Installing All Dependencies
### Expand Prowler's Capabilities
Prowler is constantly evolving. Contributions to checks, services, or integrations help improve the tool for everyone. Here is how to get involved:
<Columns cols={2}>
<Card title="Adding New Checks" icon="shield" href="/developer-guide/checks">
Want to improve Prowler's detection capabilities for your favorite cloud provider? You can contribute by writing new checks.
</Card>
<Card title="Adding New Services" icon="server" href="/developer-guide/services">
One key service for your favorite cloud provider is missing? Add it to Prowler! Do not forget to include relevant checks to validate functionality.
</Card>
<Card title="Adding New Providers" icon="cloud" href="/developer-guide/provider">
If you would like to extend Prowler to work with a new cloud provider, this typically involves setting up new services and checks to ensure compatibility.
</Card>
<Card title="Adding New Output Formats" icon="file" href="/developer-guide/outputs">
Want to tailor how results are displayed or exported? You can add custom output formats.
</Card>
<Card title="Adding New Integrations" icon="link" href="/developer-guide/integrations">
Prowler can work with other tools and platforms through integrations.
</Card>
<Card title="Proposing or Implementing Features" icon="lightbulb" href="https://github.com/prowler-cloud/prowler/issues/new?template=feature-request.yml">
Propose brand-new features or enhancements to existing ones, or help implement community-requested improvements.
</Card>
</Columns>
### Improve Documentation
Help make Prowler more accessible by enhancing our documentation, fixing typos, or adding examples/tutorials.
<Columns cols={2}>
<Card title="Documentation Guide" icon="book" href="/developer-guide/documentation">
Enhance our documentation, fix typos, or add examples/tutorials.
</Card>
</Columns>
### Bug Fixes
If you find any issues or bugs, you can report them in the [GitHub Issues](https://github.com/prowler-cloud/prowler/issues) page and if you want you can also fix them.
<Columns cols={2}>
<Card title="Report a Bug" icon="bug" href="https://github.com/prowler-cloud/prowler/issues/new?template=bug_report.yml">
Report or fix issues or bugs.
</Card>
</Columns>
Remember, our community is here to help! If you need guidance, do not hesitate to ask questions in the issues or join our [<Icon icon="slack" /> Slack workspace](https://goto.prowler.com/slack).
## Setting up your development environment
### Prerequisites
@@ -26,11 +77,11 @@ Before proceeding, ensure the following:
### Forking the Prowler Repository
To contribute to Prowler, fork the Prowler GitHub repository. This allows you to propose changes, submit new features, and fix bugs. For guidance on forking, refer to the [official GitHub documentation](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/working-with-forks/fork-a-repo?tool=webui#forking-a-repository).
Fork the Prowler GitHub repository to contribute to Prowler. This allows proposing changes, submitting new features, and fixing bugs. For guidance on forking, refer to the [official GitHub documentation](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/working-with-forks/fork-a-repo?tool=webui#forking-a-repository).
### Cloning Your Forked Repository
Once your fork is created, clone it using the following commands:
Once your fork is created, clone it using the following commands (replace `<your-github-user>` with your GitHub username):
```
git clone https://github.com/<your-github-user>/prowler
@@ -56,39 +107,7 @@ If your poetry version is below 2.0.0 you must keep using `poetry shell` to acti
In case you have any doubts, consult the [Poetry environment activation guide](https://python-poetry.org/docs/managing-environments/#activating-the-environment).
</Warning>
## Contributing to Prowler
### Ways to Contribute
Here are some ideas for collaborating with Prowler:
1. **Review Current Issues**: Check out our [GitHub Issues](https://github.com/prowler-cloud/prowler/issues) page. We often tag issues as `good first issue` - these are perfect for new contributors as they are typically well-defined and manageable in scope.
2. **Expand Prowler's Capabilities**: Prowler is constantly evolving, and you can be a part of its growth. Whether you are adding checks, supporting new services, or introducing integrations, your contributions help improve the tool for everyone. Here is how you can get involved:
- **Adding New Checks**
Want to improve Prowler's detection capabilities for your favorite cloud provider? You can contribute by writing new checks. To get started, follow the [create a new check guide](/developer-guide/checks).
- **Adding New Services**
One key service for your favorite cloud provider is missing? Add it to Prowler! To add a new service, check out the [create a new service guide](/developer-guide/services). Do not forget to include relevant checks to validate functionality.
- **Adding New Providers**
If you would like to extend Prowler to work with a new cloud provider, follow the [create a new provider guide](/developer-guide/provider). This typically involves setting up new services and checks to ensure compatibility.
- **Adding New Output Formats**
Want to tailor how results are displayed or exported? You can add custom output formats by following the [create a new output format guide](/developer-guide/outputs).
- **Adding New Integrations**
Prowler can work with other tools and platforms through integrations. If you would like to add one, see the [create a new integration guide](/developer-guide/integrations).
- **Proposing or Implementing Features**
Got an idea to make Prowler better? Whether it is a brand-new feature or an enhancement to an existing one, you are welcome to propose it or help implement community-requested improvements.
3. **Improve Documentation**: Help make Prowler more accessible by enhancing our documentation, fixing typos, or adding examples/tutorials. See the tutorial of how we write our documentation [here](/developer-guide/documentation).
4. **Bug Fixes**: If you find any issues or bugs, you can report them in the [GitHub Issues](https://github.com/prowler-cloud/prowler/issues) page and if you want you can also fix them.
Remember, our community is here to help! If you need guidance, do not hesitate to ask questions in the issues or join our [Slack workspace](https://goto.prowler.com/slack).
### Pre-Commit Hooks
@@ -121,6 +140,16 @@ These should have been already installed if `poetry install --with dev` was alre
Additionally, ensure the latest version of [`TruffleHog`](https://github.com/trufflesecurity/trufflehog) is installed to scan for sensitive data in the code. Follow the official [installation guide](https://github.com/trufflesecurity/trufflehog?tab=readme-ov-file#floppy_disk-installation) for setup.
### AI-Driven Contributions
If you are using AI assistants to help with your contributions, Prowler provides specialized resources to enhance AI-driven development:
- **Prowler MCP Server**: The [Prowler MCP Server](/getting-started/products/prowler-mcp) provides AI assistants with access to the entire Prowler ecosystem, including security checks, compliance frameworks, documentation, and more. This enables AI tools to better understand Prowler's architecture and help you create contributions that align with project standards.
- **AGENTS.md Files**: Each component of the Prowler monorepo includes an `AGENTS.md` file that contains specific guidelines for AI agents working on that component. These files provide context about project structure, coding standards, and best practices. When working on a specific component, refer to the relevant `AGENTS.md` file (e.g., `prowler/AGENTS.md`, `ui/AGENTS.md`, `api/AGENTS.md`) to ensure your AI assistant follows the appropriate guidelines.
These resources help ensure that AI-assisted contributions maintain consistency with Prowler's codebase and development practices.
### Dependency Management
All dependencies are listed in the `pyproject.toml` file.
@@ -133,7 +162,7 @@ If you encounter issues when committing to the Prowler repository, use the `--no
</Note>
### Repository Folder Structure
Understanding the layout of the Prowler codebase will help you quickly find where to add new features, checks, or integrations. The following is a high-level overview from the root of the repository:
The Prowler codebase layout helps quickly locate where to add new features, checks, or integrations. The following is a high-level overview from the root of the repository:
```
prowler/
@@ -148,7 +177,7 @@ prowler/
├── permissions/ # Permission-related files and policies
├── contrib/ # Community-contributed scripts or modules
├── kubernetes/ # Kubernetes deployment files
├── .github/ # GitHub related files (workflows, issue templates, etc.)
├── .github/ # GitHub-related files (workflows, issue templates, etc.)
├── pyproject.toml # Python project configuration (Poetry)
├── poetry.lock # Poetry lock file
├── README.md # Project overview and getting started
@@ -158,19 +187,23 @@ prowler/
└── ... # Other supporting files
```
## Pull Request Checklist
## Sending the Pull Request
When creating or reviewing a pull request in https://github.com/prowler-cloud/prowler, follow [this checklist](https://github.com/prowler-cloud/prowler/blob/master/.github/pull_request_template.md#checklist).
When creating or reviewing a pull request in <Icon icon="github" /> [Prowler](https://github.com/prowler-cloud/prowler), follow [this template](https://github.com/prowler-cloud/prowler/blob/master/.github/pull_request_template.md) and fill it with the relevant information:
- **Context** and **Description** of the change: This will help the reviewers to understand the change and the purpose of the pull request.
- **Steps to review**: A detailed description of how to review the change.
- **Checklist**: A mandatory checklist of the things that should be reviewed before merging the pull request.
## Contribution Appreciation
If you enjoy swag, we’d love to thank you for your contribution with laptop stickers or other Prowler merchandise!
If you enjoy swag, we'd love to thank you for your contribution with laptop stickers or other Prowler merchandise!
To request swag: Share your pull request details in our [Slack workspace](https://goto.prowler.com/slack).
You can also reach out to Toni de la Fuente on [Twitter](https://twitter.com/ToniBlyx)—his DMs are open!
# Testing a Pull Request from a Specific Branch
## Testing a Pull Request from a Specific Branch
To test Prowler from a specific branch (for example, to try out changes from a pull request before it is merged), you can use `pipx` to install directly from GitHub:
@@ -179,3 +212,5 @@ pipx install "git+https://github.com/prowler-cloud/prowler.git@branch-name"
```
Replace `branch-name` with the name of the branch you want to test. This will install Prowler in an isolated environment, allowing you to try out the changes safely.
For more details on testing go to the [Testing section](/developer-guide/unit-testing) of this documentation.
+1 -1
View File
@@ -4,7 +4,7 @@ title: 'Kubernetes Provider'
This page details the [Kubernetes](https://kubernetes.io/) provider implementation in Prowler.
By default, Prowler will audit all namespaces in the Kubernetes cluster accessible by the configured context. To configure it, see the [In-Cluster Execution](/user-guide/providers/kubernetes/in-cluster) or [Non In-Cluster Execution](/user-guide/providers/kubernetes/outside-cluster) guides.
By default, Prowler will audit all namespaces in the Kubernetes cluster accessible by the configured context. To configure it, see the [In-Cluster Execution](/user-guide/providers/kubernetes/getting-started-k8s#in-cluster-execution) or [Non In-Cluster Execution](/user-guide/providers/kubernetes/getting-started-k8s#non-in-cluster-execution) guides.
## Kubernetes Provider Classes Architecture
+2 -2
View File
@@ -110,6 +110,7 @@
]
},
"user-guide/tutorials/prowler-app-lighthouse",
"user-guide/tutorials/prowler-cloud-public-ips",
{
"group": "Tutorials",
"pages": [
@@ -194,8 +195,7 @@
{
"group": "Kubernetes",
"pages": [
"user-guide/providers/kubernetes/in-cluster",
"user-guide/providers/kubernetes/outside-cluster",
"user-guide/providers/kubernetes/getting-started-k8s",
"user-guide/providers/kubernetes/misc"
]
},
+10 -1
View File
@@ -5,7 +5,7 @@ title: "Overview"
**Prowler MCP Server** brings the entire Prowler ecosystem to AI assistants through the Model Context Protocol (MCP). It enables seamless integration with AI tools like Claude Desktop, Cursor, and other MCP clients, allowing interaction with Prowler's security capabilities through natural language.
<Warning>
**Preview Feature**: This MCP server is currently in preview and under active development. Features and functionality may change. We welcome your feedback—please report any issues on [GitHub](https://github.com/prowler-cloud/prowler/issues) or join our [Slack community](https://goto.prowler.com/slack) to discuss and share your thoughts.
**Preview Feature**: This MCP server is currently under active development. Features and functionality may change. We welcome your feedback—please report any issues on [GitHub](https://github.com/prowler-cloud/prowler/issues) or join our [Slack community](https://goto.prowler.com/slack) to discuss and share your thoughts.
</Warning>
## What is the Model Context Protocol?
@@ -42,6 +42,15 @@ Search and retrieve official Prowler documentation:
- **Contextual Results**: Get relevant documentation pages with highlighted snippets.
- **Document Retrieval**: Access complete markdown content of any documentation file.
## MCP Server Architecture
The following diagram illustrates the Prowler MCP Server architecture and its integration points:
<img className="block dark:hidden" src="/images/prowler_mcp_schema_light.png" alt="Prowler MCP Server Schema" />
<img className="hidden dark:block" src="/images/prowler_mcp_schema_dark.png" alt="Prowler MCP Server Schema" />
The architecture shows how AI assistants connect through the MCP protocol to access Prowler's three main components: Prowler Cloud/App for security operations, Prowler Hub for security knowledge, and Prowler Documentation for guidance and reference.
## Use Cases
The Prowler MCP Server enables powerful workflows through AI assistants:
Binary file not shown.

After

Width:  |  Height:  |  Size: 328 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 332 KiB

+1 -1
View File
@@ -28,7 +28,7 @@ The supported providers right now are:
| [AWS](/user-guide/providers/aws/getting-started-aws) | Official | UI, API, CLI |
| [Azure](/user-guide/providers/azure/getting-started-azure) | Official | UI, API, CLI |
| [Google Cloud](/user-guide/providers/gcp/getting-started-gcp) | Official | UI, API, CLI |
| [Kubernetes](/user-guide/providers/kubernetes/in-cluster) | Official | UI, API, CLI |
| [Kubernetes](/user-guide/providers/kubernetes/getting-started-k8s) | Official | UI, API, CLI |
| [M365](/user-guide/providers/microsoft365/getting-started-m365) | Official | UI, API, CLI |
| [Github](/user-guide/providers/github/getting-started-github) | Official | UI, API, CLI |
| [Oracle Cloud](/user-guide/providers/oci/getting-started-oci) | Official | UI, API, CLI |
+2 -2
View File
@@ -24,7 +24,7 @@ Standard results will be shown and additionally the framework information as the
**If Prowler can't find a resource related with a check from a compliance requirement, this requirement won't appear on the output**
</Note>
## List Available Compliance Frameworks
## List Available Compliance Frameworks
To see which compliance frameworks are covered by Prowler, use the `--list-compliance` option:
@@ -34,7 +34,7 @@ prowler <provider> --list-compliance
Or you can visit [Prowler Hub](https://hub.prowler.com/compliance).
## List Requirements of Compliance Frameworks
## List Requirements of Compliance Frameworks
To list requirements for a compliance framework, use the `--list-compliance-requirements` option:
```sh
@@ -94,7 +94,7 @@ The following list includes all the Azure checks with configurable variables tha
### Configurable Checks
## Kubernetes
## Kubernetes
### Configurable Checks
The following list includes all the Kubernetes checks with configurable variables that can be changed in the configuration yaml file:
@@ -2,7 +2,7 @@
title: 'Integrations'
---
## Integration with Slack
## Integration with Slack
Prowler can be integrated with [Slack](https://slack.com/) to send a summary of the execution having configured a Slack APP in your channel with the following command:
+3 -3
View File
@@ -14,7 +14,7 @@ prowler <provider> -V/-v/--version
Prowler provides various execution settings.
### Verbose Execution
### Verbose Execution
To enable verbose mode in Prowler, similar to Version 2, use:
@@ -54,7 +54,7 @@ To run Prowler without color formatting:
prowler <provider> --no-color
```
### Checks in Prowler
### Checks in Prowler
Prowler provides various security checks per cloud provider. Use the following options to list, execute, or exclude specific checks:
@@ -96,7 +96,7 @@ prowler <provider> -e/--excluded-checks ec2 rds
prowler <provider> -C/--checks-file <checks_list>.json
```
## Custom Checks in Prowler
## Custom Checks in Prowler
Prowler supports custom security checks, allowing users to define their own logic.
+104 -11
View File
@@ -19,15 +19,45 @@ The Mutelist option works in combination with other filtering mechanisms and mod
## How the Mutelist Works
The **Mutelist** uses both "AND" and "OR" logic to determine which resources, checks, regions, and tags should be muted. For each check, the Mutelist evaluates whether the account, region, and resource match the specified criteria using "AND" logic. If tags are specified, the Mutelist can apply either "AND" or "OR" logic.
The **Mutelist** uses **AND logic** to evaluate whether a finding should be muted. For a finding to be muted, **ALL** of the following conditions must match:
If any of the criteria do not match, the check is not muted.
- **Account** matches (exact match or `*`)
- **Check** matches (exact match, regex pattern, or `*`)
- **Region** matches (exact match, regex pattern, or `*`)
- **Resource** matches (exact match, regex pattern, or `*`)
- **Tags** match (if specified)
If **any** of these criteria do not match, the finding is **not muted**.
### Tag Matching Logic
Tags have special matching behavior:
- **Multiple tags in the list = AND logic**: ALL tags must be present on the resource
```yaml
Tags:
- "environment=dev"
- "team=backend" # BOTH tags required
```
- **Regex alternation within a single tag = OR logic**: Use the pipe operator `|` for OR
```yaml
Tags:
- "environment=dev|environment=stg" # Matches EITHER dev OR stg
```
- **Complex tag patterns**: Combine AND and OR using regex
```yaml
Tags:
- "team=backend" # Required
- "environment=dev|environment=stg" # AND (dev OR stg)
```
<Note>
Remember that mutelist can be used with regular expressions.
</Note>
## Mutelist Specification
## Mutelist Specification
<Note>
- For Azure provider, the Account ID is the Subscription Name and the Region is the Location.
@@ -40,9 +70,10 @@ The Mutelist file uses the [YAML](https://en.wikipedia.org/wiki/YAML) format wit
```yaml
### Account, Check and/or Region can be * to apply for all the cases.
### Resources and tags are lists that can have either Regex or Keywords.
### Tags is an optional list that matches on tuples of 'key=value' and are "ANDed" together.
### Use an alternation Regex to match one of multiple tags with "ORed" logic.
### For each check you can except Accounts, Regions, Resources and/or Tags.
### Multiple tags in the list are "ANDed" together (ALL must match).
### Use regex alternation (|) within a single tag for "OR" logic (e.g., "env=dev|env=stg").
### For each check you can use Exceptions to unmute specific Accounts, Regions, Resources and/or Tags.
### All conditions (Account, Check, Region, Resource, Tags) are ANDed together.
########################### MUTELIST EXAMPLE ###########################
Mutelist:
Accounts:
@@ -148,11 +179,11 @@ Mutelist:
| Field| Description| Logic
|----------|----------|----------
| `account_id`| Use `*` to apply the mutelist to all accounts.| `ANDed`
| `check_name`| The name of the Prowler check. Use `*` to apply the mutelist to all checks, or `service_*` to apply it to all service's checks.| `ANDed`
| `region`| The region identifier. Use `*` to apply the mutelist to all regions.| `ANDed`
| `resource`| The resource identifier. Use `*` to apply the mutelist to all resources.| `ANDed`
| `tag`| The tag value.| `ORed`
| `account_id`| Use `*` to apply the mutelist to all accounts. Supports exact match or wildcard.| `AND` (with other fields)
| `check_name`| The name of the Prowler check. Use `*` to apply the mutelist to all checks, or `service_*` to apply it to all service's checks. Supports regex patterns.| `AND` (with other fields)
| `region`| The region identifier. Use `*` to apply the mutelist to all regions. Supports regex patterns.| `AND` (with other fields)
| `resource`| The resource identifier. Use `*` to apply the mutelist to all resources. Supports regex patterns.| `AND` (with other fields)
| `tags`| List of tag patterns in `key=value` format. **Multiple tags = AND** (all must match). **Regex alternation within single tag = OR** (use `tag1\|tag2`).| `AND` between tags, `OR` within regex
### Description
@@ -173,6 +204,68 @@ Replace `<provider>` with the appropriate provider name.
- The Mutelist can be used in combination with other Prowler options, such as the `--service` or `--checks` option, to further customize the scanning process.
- Make sure to review and update the Mutelist regularly to ensure it reflects the desired exclusions and remains up to date with your infrastructure.
## Current Limitations and Workarounds
### Limitation: No OR Logic Between Different Rule Sets
The current Mutelist schema **does not support OR logic** between different condition sets. Each check can have only **one rule object**, and all conditions are **ANDed** together.
**Example of unsupported scenario:**
```yaml
# ❌ INVALID: Cannot have multiple rule blocks for the same check
Accounts:
"*":
Checks:
"*": # Rule 1
Regions: ["eu-west-1", "us-west-2"]
Resources: ["*"]
"*": # Rule 2 - This will OVERWRITE Rule 1 (YAML duplicate key)
Regions: ["us-east-1"]
Tags: ["environment=dev"]
```
**Workaround: Use multiple scans with different mutelists**
For complex scenarios requiring OR logic, run separate scans:
```bash
# Scan 1: Mute findings in non-critical regions
prowler aws --mutelist-file mutelist_noncritical.yaml
# Scan 2: Mute dev/stg in critical regions
prowler aws --mutelist-file mutelist_critical.yaml --regions us-east-1,sa-east-1
```
Then merge the outputs in your reporting pipeline.
### Limitation: Cannot Negate Regions
You cannot express "all regions **except** X and Y". You must explicitly list all regions you want to mute.
**Workaround:**
```yaml
# Must enumerate all unwanted regions
Accounts:
"*":
Checks:
"*":
Regions:
- "af-south-1"
- "ap-east-1"
# ... list all regions EXCEPT the ones you want to monitor
Resources: ["*"]
```
### Best Practices
1. **Use regex patterns for flexibility**: Instead of listing multiple resources, use regex patterns like `"dev-.*"` or `"test-instance-[0-9]+"`
2. **Combine tag OR logic with regex**: Use `"environment=dev|environment=stg|environment=test"` instead of multiple tag entries
3. **Be specific with exceptions**: Use the `Exceptions` field to unmute specific resources within a broader muting rule
4. **Test your mutelist**: Run Prowler with `--output-modes json` and verify that the expected findings are muted
## AWS Mutelist
### Muting specific AWS regions
@@ -10,7 +10,7 @@ This can help for really large accounts, but please be aware of AWS API rate lim
2. **API Rate Limits**: Most of the rate limits in AWS are applied at the API level. Each API call to an AWS service counts towards the rate limit for that service.
3. **Throttling Responses**: When you exceed the rate limit for a service, AWS responds with a throttling error. In AWS SDKs, these are typically represented as `ThrottlingException` or `RateLimitExceeded` errors.
For information on Prowler's retrier configuration please refer to this [page](https://docs.prowler.cloud/en/latest/tutorials/aws/boto3-configuration/).
For information on Prowler's retrier configuration please refer to this [page](https://docs.prowler.com/user-guide/providers/aws/boto3-configuration/).
<Note>
You might need to increase the `--aws-retries-max-attempts` parameter from the default value of 3. The retrier follows an exponential backoff strategy.
@@ -24,6 +24,6 @@ By default, it extracts resources from all the regions, you could use `-f`/`--fi
![Quick Inventory Example](/images/quick-inventory.jpg)
## Objections
## Objections
The inventorying process is carried out with `resourcegroupstaggingapi` calls, which means that only resources they have or have had tags will appear (except for the IAM and S3 resources which are done with Boto3 API calls).
+5 -5
View File
@@ -22,7 +22,7 @@ prowler <provider> --output-formats json-asff
All compliance-related reports are automatically generated when Prowler is executed. These outputs are stored in the `/output/compliance` directory.
## Custom Output Flags
## Custom Output Flags
By default, Prowler creates a file inside the `output` directory named: `prowler-output-ACCOUNT_NUM-OUTPUT_DATE.format`.
@@ -53,13 +53,13 @@ Both flags can be used simultaneously to provide a custom directory and filename
By default, the timestamp format of the output files is ISO 8601. This can be changed with the flag `--unix-timestamp` generating the timestamp fields in pure unix timestamp format.
## Supported Output Formats
## Supported Output Formats
Prowler natively supports the following reporting output formats:
- CSV
- JSON-OCSF
- JSON-ASFF
- JSON-ASFF (AWS only)
- HTML
Hereunder is the structure for each of the supported report formats by Prowler:
@@ -285,10 +285,10 @@ The JSON-OCSF output format implements the [Detection Finding](https://schema.oc
Each finding is a `json` object within a list.
</Note>
### JSON-ASFF
### JSON-ASFF (AWS Only)
<Note>
Only available when using `--security-hub` or `--output-formats json-asff`
Only available when using `--security-hub` or `--output-formats json-asff` with the AWS provider.
</Note>
The following code is an example output of the [JSON-ASFF](https://docs.aws.amazon.com/securityhub/latest/userguide/securityhub-findings-format-syntax.html) format:
@@ -14,7 +14,7 @@ prowler <provider> --scan-unused-services
## Services Ignored
### AWS
### AWS
#### ACM (AWS Certificate Manager)
@@ -22,21 +22,21 @@ Certificates stored in ACM without active usage in AWS resources are excluded. B
- `acm_certificates_expiration_check`
#### Athena
#### Athena
Upon AWS account creation, Athena provisions a default primary workgroup for the user. Prowler verifies if this workgroup is enabled and used by checking for queries within the last 45 days. If Athena is unused, findings related to its checks will not appear.
- `athena_workgroup_encryption`
- `athena_workgroup_enforce_configuration`
#### AWS CloudTrail
#### AWS CloudTrail
AWS CloudTrail should have at least one trail with a data event to record all S3 object-level API operations. Before flagging this issue, Prowler verifies if S3 buckets exist in the account.
- `cloudtrail_s3_dataevents_read_enabled`
- `cloudtrail_s3_dataevents_write_enabled`
#### AWS Elastic Compute Cloud (EC2)
#### AWS Elastic Compute Cloud (EC2)
If Amazon Elastic Block Store (EBS) default encyption is not enabled, sensitive data at rest will remain unprotected in EC2. However, Prowler will only generate a finding if EBS volumes exist where default encryption could be enforced.
@@ -56,7 +56,7 @@ Prowler scans only attached security groups to report vulnerabilities in activel
- `ec2_networkacl_allow_ingress_X_port`
#### AWS Glue
#### AWS Glue
AWS Glue best practices recommend encrypting metadata and connection passwords in Data Catalogs.
@@ -71,7 +71,7 @@ Amazon Inspector is a vulnerability discovery service that automates continuous
- `inspector2_is_enabled`
#### Amazon Macie
#### Amazon Macie
Amazon Macie leverages machine learning to automatically discover, classify, and protect sensitive data in S3 buckets. Prowler only generates findings if Macie is disabled and there are S3 buckets in the AWS account.
@@ -83,7 +83,7 @@ A network firewall is essential for monitoring and controlling traffic within a
- `networkfirewall_in_all_vpc`
#### Amazon S3
#### Amazon S3
To prevent unintended data exposure:
@@ -91,7 +91,7 @@ Public Access Block should be enabled at the account level. Prowler only checks
- `s3_account_level_public_access_blocks`
#### Virtual Private Cloud (VPC)
#### Virtual Private Cloud (VPC)
VPC settings directly impact network security and availability.
@@ -84,6 +84,17 @@ For detailed instructions on how to create the role, see [Authentication > Assum
![Next button in Prowler Cloud](/images/providers/next-button-prowler-cloud.png)
![Launch Scan](/images/providers/launch-scan-button-prowler-cloud.png)
<Note>
Check if your AWS Security Token Service (STS) has the EU (Ireland) endpoint active. If not, we will not be able to connect to your AWS account.
If that is the case your STS configuration may look like this:
<img src="/images/sts-configuration.png" alt="AWS Role" width="800" />
To solve this issue, please activate the EU (Ireland) STS endpoint.
</Note>
---
#### Credentials (Static Access Keys)
@@ -53,7 +53,8 @@ For detailed instructions on how to create the Service Principal and configure p
### Step 3: Add Credentials to Prowler App
Having completed the [Service Principal setup from the Authentication guide](/user-guide/providers/azure/authentication#service-principal-application-authentication-recommended):
For Azure, Prowler App uses a service principal application to authenticate. For more information about the process of creating and adding permissions to a service principal refer to this [section](/user-guide/providers/azure/authentication). When you finish creating and adding the [Entra](/user-guide/providers/azure/create-prowler-service-principal#assigning-proper-permissions) and [Subscription](/user-guide/providers/azure/subscriptions) scope permissions to the service principal, enter the `Tenant ID`, `Client ID` and `Client Secret` of the service principal application.
1. Go to your App Registration overview and copy the `Client ID` and `Tenant ID`
@@ -14,7 +14,10 @@ Prowler for Google Cloud supports multiple authentication methods. To use a spec
Prowler for Google Cloud requires the following permissions:
### IAM Roles
- **Reader (`roles/reader`)** – Must be granted at the **project, folder, or organization** level to allow scanning of target projects.
- **Viewer (`roles/viewer`)** – Must be granted at the **project, folder, or organization** level to allow scanning of target projects.
- **Service Usage Consumer (`roles/serviceusage.serviceUsageConsumer`)** IAM Role – Required for resource scanning.
- **Custom `ProwlerRole`** – Include granular permissions that are not included in the Viewer role:
- `storage.buckets.getIamPolicy`
### Project-Level Settings
@@ -106,18 +109,46 @@ prowler gcp --project-ids <project-id>
This method uses a service account with a downloaded key file for authentication.
### Create Service Account and Key
### Step 1: Create ProwlerRole
1. Go to the [Service Accounts page](https://console.cloud.google.com/iam-admin/serviceaccounts) in the GCP Console
2. Click "Create Service Account"
3. Fill in the service account details and click "Create and Continue"
4. Grant the service account the "Reader" role
5. Click "Done"
6. Find your service account in the list and click on it
7. Go to the "Keys" tab
8. Click "Add Key" > "Create new key"
9. Select "JSON" and click "Create"
10. Save the downloaded key file securely
To keep permissions focused:
1. Create a custom role named **ProwlerRole** that explicitly includes the permissions your compliance team approves. Click **Create role**, set the title to *ProwlerRole*, keep the ID readable (for example, `prowler_role`)
2. Add the required permission `storage.buckets.getIamPolicy` (the permission highlighted in the screenshots). To make it easier, filter the permissions by `Storage Admin` role.
![Create a custom Prowler role](/user-guide/providers/gcp/img/roles-section.png)
![Sample permissions for a custom Prowler role](/user-guide/providers/gcp/img/prowler-role.png)
### Step 2: Create the Service Account
1. Navigate to **IAM & Admin > Service Accounts** and make sure the correct project is selected.
![Service accounts landing page](/user-guide/providers/gcp/img/service-account-page.png)
2. Select **Create service account**, provide a name, ID, and a short description that states the purpose (for example, “Service account to execute Prowler”), then click **Create and continue**.
![Create service account wizard](/user-guide/providers/gcp/img/create-service-account.png)
3. Assign the roles you prepared earlier:
- **ProwlerRole** for `cloudstorage` service checks.
- **Viewer** for broad read-only visibility.
- **Service Usage Consumer** so Prowler can inspect API states.
![Assign roles to the service account](/user-guide/providers/gcp/img/service-account-permissions.png)
4. Continue through the wizard and finish. No principals need to be granted access in step 3 unless you want other identities to impersonate this account.
### Step 3: Generate a JSON Key
1. Open the newly created service account, move to the **Keys** tab, and choose **Add key > Create new key**.
![Add a new key to the service account](/user-guide/providers/gcp/img/create-new-key.png)
2. Select **JSON** as the key type and click **Create**. The browser downloads the file exactly once.
![Select JSON as the key type](/user-guide/providers/gcp/img/json-key.png)
3. Once created, make sure to store the Key securely.
### Using with Prowler CLI
@@ -32,35 +32,45 @@ title: 'Getting Started With GCP on Prowler'
### Step 3: Set Up GCP Authentication
Choose the preferred authentication mode before proceeding:
For Google Cloud, first enter your `GCP Project ID` and then select the authentication method you want to use:
**User Credentials (Application Default Credentials)**
- **Service Account Authentication** (**Recommended**)
* Authenticates as a service identity
* Stable and auditable
* Recommended for production
- **Application Default Credentials**
* Quick scan as current user
* Uses Google Cloud CLI authentication
* Credentials may time out
* Quick scan as current user
* Uses Google Cloud CLI authentication
* Credentials may time out
**Service Account Authentication** is the recommended authentication method for automated systems and machine-to-machine interactions, like Prowler. For detailed information about this, refer to the [Google Cloud documentation](https://cloud.google.com/iam/docs/service-account-overview).
**Service Account Key File**
<img src="/images/prowler-app/gcp-auth-methods.png" alt="GCP Authentication Methods" width="700" />
* Authenticates as a service identity
* Stable and auditable
* Recommended for production
<Tabs>
<Tab title="Service Account Authentication">
First of all, in the same project that you selected in the previous step, you need to create a service account and then generate a key in JSON format for it. For more information about this, you can follow the next Google Cloud documentation tutorials:
For detailed instructions on how to set up authentication, see [Authentication](/user-guide/providers/gcp/authentication).
- [Create a service account](https://cloud.google.com/iam/docs/creating-managing-service-accounts)
- [Generate a key for a service account](https://cloud.google.com/iam/docs/creating-managing-service-account-keys)
6. Once credentials are configured, return to Prowler App and enter the required values:
<img src="/images/prowler-app/gcp-service-account-creds.png" alt="GCP Service Account Credentials" width="700" />
</Tab>
<Tab title="Application Default Credentials">
1. Run the following command in your terminal to authenticate with GCP:
For "Service Account Key":
```bash
gcloud auth application-default login
```
- `Service Account Key JSON`
2. Once authenticated, get the `Client ID`, `Client Secret` and `Refresh Token` from `~/.config/gcloud/application_default_credentials`.
For "Application Default Credentials":
3. Paste the `Client ID`, `Client Secret` and `Refresh Token` into Prowler App.
- `client_id`
- `client_secret`
- `refresh_token`
<img src="/images/gcp-credentials.png" alt="GCP Credentials" width="700" />
</Tab>
</Tabs>
![Enter the Credentials](/images/providers/enter-credentials-prowler-cloud.png)
7. Click "Next", then "Launch Scan"
Binary file not shown.

After

Width:  |  Height:  |  Size: 185 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 369 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 86 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 587 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 403 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 299 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 472 KiB

@@ -0,0 +1,131 @@
---
title: 'Getting Started with Kubernetes'
---
## Prowler App
### Step 1: Access Prowler Cloud/App
1. Navigate to [Prowler Cloud](https://cloud.prowler.com/) or launch [Prowler App](/user-guide/tutorials/prowler-app)
2. Go 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 "Kubernetes"
5. Enter your Kubernetes Cluster context from your kubeconfig file and optionally provide a friendly alias
### Step 2: Configure Kubernetes Authentication
For Kubernetes, Prowler App uses a `kubeconfig` file to authenticate. Paste the contents of your `kubeconfig` file into the `Kubeconfig content` field.
By default, the `kubeconfig` file is located at `~/.kube/config`.
![Kubernetes Credentials](/images/kubernetes-credentials.png)
### Step 3: Additional Setup for EKS, GKE, AKS, or External Clusters
If you are adding an **EKS**, **GKE**, **AKS** or external cluster, follow these additional steps to ensure proper authentication:
**Make sure your cluster allows traffic from the Prowler Cloud IP address `52.48.254.174/32`**
1. Apply the necessary Kubernetes resources to your EKS, GKE, AKS or external cluster (you can find the files in the [`kubernetes` directory of the Prowler repository](https://github.com/prowler-cloud/prowler/tree/master/kubernetes)):
```console
kubectl apply -f kubernetes/prowler-sa.yaml
kubectl apply -f kubernetes/prowler-role.yaml
kubectl apply -f kubernetes/prowler-rolebinding.yaml
```
2. Generate a long-lived token for authentication:
```console
kubectl create token prowler-sa -n prowler-ns --duration=0
```
- **Security Note:** The `--duration=0` option generates a non-expiring token, which may pose a security risk if not managed properly. Users should decide on an appropriate expiration time based on their security policies. If a limited-time token is preferred, set `--duration=<TIME>` (e.g., `--duration=24h`).
- **Important:** If the token expires, Prowler Cloud will no longer be able to authenticate with the cluster. In this case, you will need to generate a new token and **remove and re-add the provider in Prowler Cloud** with the updated `kubeconfig`.
3. Update your `kubeconfig` to use the ServiceAccount token:
```console
kubectl config set-credentials prowler-sa --token=<SA_TOKEN>
kubectl config set-context <CONTEXT_NAME> --user=prowler-sa
```
Replace `<SA_TOKEN>` with the generated token and `<CONTEXT_NAME>` with your KubeConfig Context Name of your EKS, GKE or AKS cluster.
4. Add the modified `kubeconfig` in Prowler Cloud and test the connection.
## Prowler CLI
### Non In-Cluster Execution
For execution outside the cluster environment, specify the location of the [kubeconfig](https://kubernetes.io/docs/concepts/configuration/organize-cluster-access-kubeconfig/) file using the following argument:
```console
prowler kubernetes --kubeconfig-file /path/to/kubeconfig
```
<Note>
If no `--kubeconfig-file` is provided, Prowler will use the default KubeConfig file location (`~/.kube/config`).
</Note>
<Note>
`prowler` will scan the active Kubernetes context by default. Use the [`--context`](https://docs.prowler.com/projects/prowler-open-source/en/latest/tutorials/kubernetes/context/) flag to specify the context to be scanned.
</Note>
<Note>
By default, `prowler` will scan all namespaces in your active Kubernetes context. Use the [`--namespace`](https://docs.prowler.com/projects/prowler-open-source/en/latest/tutorials/kubernetes/namespace/) flag to specify the namespace(s) to be scanned.
</Note>
### In-Cluster Execution
For in-cluster execution, use the supplied yaml files inside `/kubernetes`:
* [prowler-sa.yaml](https://github.com/prowler-cloud/prowler/blob/master/kubernetes/prowler-sa.yaml)
* [job.yaml](https://github.com/prowler-cloud/prowler/blob/master/kubernetes/job.yaml)
* [prowler-role.yaml](https://github.com/prowler-cloud/prowler/blob/master/kubernetes/prowler-role.yaml)
* [prowler-rolebinding.yaml](https://github.com/prowler-cloud/prowler/blob/master/kubernetes/prowler-rolebinding.yaml)
They can be used to run Prowler as a job within a new Prowler namespace:
```console
kubectl apply -f kubernetes/prowler-sa.yaml
kubectl apply -f kubernetes/job.yaml
kubectl apply -f kubernetes/prowler-role.yaml
kubectl apply -f kubernetes/prowler-rolebinding.yaml
kubectl get pods --namespace prowler-ns --> prowler-XXXXX
kubectl logs prowler-XXXXX --namespace prowler-ns
```
<Note>
By default, `prowler` will scan all namespaces in your active Kubernetes context. Use the [`--namespace`](https://docs.prowler.com/projects/prowler-open-source/en/latest/tutorials/kubernetes/namespace/) flag to specify the namespace(s) to be scanned.
</Note>
<Tip>
**Identifying the cluster in reports**
When running in in-cluster mode, the Kubernetes API does not expose the actual cluster name by default.
To uniquely identify the cluster in logs and reports:
- Use the `--cluster-name` flag to manually set the cluster name:
```bash
prowler -p kubernetes --cluster-name production-cluster
```
- Or set the `CLUSTER_NAME` environment variable:
```yaml
env:
- name: CLUSTER_NAME
value: production-cluster
```
</Tip>
@@ -1,45 +0,0 @@
---
title: 'In-Cluster Execution'
---
For in-cluster execution, use the supplied yaml files inside `/kubernetes`:
* [prowler-sa.yaml](https://github.com/prowler-cloud/prowler/blob/master/kubernetes/prowler-sa.yaml)
* [job.yaml](https://github.com/prowler-cloud/prowler/blob/master/kubernetes/job.yaml)
* [prowler-role.yaml](https://github.com/prowler-cloud/prowler/blob/master/kubernetes/prowler-role.yaml)
* [prowler-rolebinding.yaml](https://github.com/prowler-cloud/prowler/blob/master/kubernetes/prowler-rolebinding.yaml)
They can be used to run Prowler as a job within a new Prowler namespace:
```console
kubectl apply -f kubernetes/prowler-sa.yaml
kubectl apply -f kubernetes/job.yaml
kubectl apply -f kubernetes/prowler-role.yaml
kubectl apply -f kubernetes/prowler-rolebinding.yaml
kubectl get pods --namespace prowler-ns --> prowler-XXXXX
kubectl logs prowler-XXXXX --namespace prowler-ns
```
<Note>
By default, `prowler` will scan all namespaces in your active Kubernetes context. Use the [`--namespace`](https://docs.prowler.com/projects/prowler-open-source/en/latest/tutorials/kubernetes/namespace/) flag to specify the namespace(s) to be scanned.
</Note>
<Tip>
**Identifying the cluster in reports**
When running in in-cluster mode, the Kubernetes API does not expose the actual cluster name by default.
To uniquely identify the cluster in logs and reports, you can:
- Use the `--cluster-name` flag to manually set the cluster name:
```bash
prowler -p kubernetes --cluster-name production-cluster
```
- Or set the `CLUSTER_NAME` environment variable:
```yaml
env:
- name: CLUSTER_NAME
value: production-cluster
```
</Tip>
@@ -1,22 +0,0 @@
---
title: 'Non In-Cluster Execution'
---
For execution outside the cluster environment, specify the location of the [kubeconfig](https://kubernetes.io/docs/concepts/configuration/organize-cluster-access-kubeconfig/) file using the following argument:
```console
prowler kubernetes --kubeconfig-file /path/to/kubeconfig
```
<Note>
If no `--kubeconfig-file` is provided, Prowler will use the default KubeConfig file location (`~/.kube/config`).
</Note>
<Note>
`prowler` will scan the active Kubernetes context by default. Use the [`--context`](https://docs.prowler.com/projects/prowler-open-source/en/latest/tutorials/kubernetes/context/) flag to specify the context to be scanned.
</Note>
<Note>
By default, `prowler` will scan all namespaces in your active Kubernetes context. Use the [`--namespace`](https://docs.prowler.com/projects/prowler-open-source/en/latest/tutorials/kubernetes/namespace/) flag to specify the namespace(s) to be scanned.
</Note>
@@ -50,15 +50,15 @@ Configure authentication for Microsoft 365 by following the [Microsoft 365 Authe
### Step 3: Select Authentication Method and Provide Credentials
Prowler App now separates Microsoft 365 authentication into two app-only options. After adding the Domain ID, choose the method that matches your setup:
Prowler App now separates Microsoft 365 authentication into two app-only options. After adding the Domain ID (primary tenant domain), choose the method that matches your setup:
<img src="/images/providers/m365-auth-selection-form.png" alt="M365 authentication method selection" width="700" />
#### Application Certificate Authentication (Recommended)
1. Copy the Application (client) ID and Tenant ID from the app registration overview page.
2. Paste both values into the Prowler App form.
3. Upload the PFX bundle or paste the Base64-encoded certificate (`M365_CERTIFICATE_CONTENT`), then click **Test Connection**.
1. Enter your **tenant ID**: This is the unique identifier for your Microsoft Entra ID directory.
2. Enter your **application (client) ID**: This is the unique identifier assigned to your app registration in Microsoft Entra ID.
3. Upload your **certificate file content**: This is the Base64 encoded certificate content used to authenticate your application.
<img src="/images/providers/certificate-form.png" alt="M365 certificate authentication form" width="700" />
@@ -66,9 +66,9 @@ Use this method whenever possible to avoid managing client secrets and to unlock
#### Application Client Secret Authentication
1. From the app registration, copy the Application (client) ID and Tenant ID.
2. Paste both values plus the client secret into the Prowler App form.
3. Click **Test Connection** to validate the credentials.
1. Enter your **tenant ID**: This is the unique identifier for your Microsoft Entra ID directory.
2. Enter your **application (client) ID**: This is the unique identifier assigned to your app registration in Microsoft Entra ID.
3. Enter your **client secret**: This is the secret key used to authenticate your application.
<img src="/images/providers/secret-form.png" alt="M365 client secret authentication form" width="700" />
@@ -142,7 +142,7 @@ The following API endpoints are accessible to Prowler Lighthouse AI. Data from t
**Compliance Management:**
- List compliance overviews for a scan - `/api/v1/compliance-overviews`
- List compliance overviews (optionally filter by scan) - `/api/v1/compliance-overviews`
- Retrieve data from a specific compliance overview - `/api/v1/compliance-overviews/{id}`
#### Excluded API Endpoints
@@ -67,7 +67,7 @@ The Mutelist configuration takes effect on the next scans.
</Note>
## Mutelist Ready To Use Examples
Below are examples for different cloud providers supported by Prowler App. Check how the mutelist works [here](https://docs.prowler.com/projects/prowler-open-source/en/latest/tutorials/mutelist/#how-the-mutelist-works).
Below are examples for different cloud providers supported by Prowler App. Check how the mutelist works [here](/user-guide/cli/tutorials/mutelist#how-the-mutelist-works).
### AWS Provider
+41 -33
View File
@@ -10,9 +10,9 @@ This guide provides comprehensive instructions to configure SAML-based Single Si
This document is divided into two main sections:
- **User Guide**: For organization administrators to configure SAML SSO through Prowler App.
- **[User Guide](#user-guide-configuration)**: For organization administrators to configure SAML SSO through Prowler App.
- **Developer and Administrator Guide**: For developers and system administrators running self-hosted Prowler App instances, providing technical details on environment configuration, API usage, and testing.
- **[Developer and Administrator Guide](#developer-and-administrator-guide)**: For developers and system administrators running self-hosted Prowler App instances, providing technical details on environment configuration, API usage, and testing.
---
@@ -53,58 +53,65 @@ On the profile page, find the "SAML SSO Integration" card and click "Enable" to
![Enable SAML Integration](/images/prowler-app/saml/saml-step-2.png)
<Info>
**Choose Your Method**
Next section will explain how to fill the IdP configuration based on your Identity Provider.
**Use Step 3A (Generic Method)** for any SAML 2.0 compliant Identity Provider or when you need custom configuration.
#### Step 3: Configure the Identity Provider (IdP)
Choose a Method:
**Use Step 3B (Okta App Catalog)** if you're using Okta and want a simplified setup process with pre-configured settings.
- Use [**Generic Method**](#generic-method) for any SAML 2.0 compliant Identity Provider or when you need custom configuration.
- Use [**Okta App Catalog**](#okta-app-catalog) if you're using Okta and want a simplified setup process with pre-configured settings.
</Info>
#### Step 3A: Configure the Identity Provider (IdP) - Generic
<Tabs>
<Tab title="Generic Method">
Prowler App displays the SAML configuration information needed to configure the IdP. Use this information to create a new SAML application in the IdP.
Prowler App displays the SAML configuration information needed to configure the IdP. Use this information to create a new SAML application in the IdP.
1. **Assertion Consumer Service (ACS) URL**: The endpoint in Prowler that will receive the SAML assertion from the IdP.
2. **Audience URI (Entity ID)**: A unique identifier for the Prowler application (Service Provider).
1. **Assertion Consumer Service (ACS) URL**: The endpoint in Prowler that will receive the SAML assertion from the IdP.
2. **Audience URI (Entity ID)**: A unique identifier for the Prowler application (Service Provider).
To configure the IdP, copy the **ACS URL** and **Audience URI** from Prowler App and use them to set up a new SAML application.
To configure the IdP, copy the **ACS URL** and **Audience URI** from Prowler App and use them to set up a new SAML application.
![IdP configuration](/images/prowler-app/saml/idp_config.png)
![IdP configuration](/images/prowler-app/saml/idp_config.png)
<Info>
**IdP Configuration**
<Info>
**IdP Configuration**
The exact steps for configuring an IdP vary depending on the provider (Okta, Azure AD, etc.). Please refer to the IdP's documentation for instructions on creating a SAML application. For SSO integration with Azure AD / Entra ID, see our [Entra ID configuration instructions](/user-guide/tutorials/prowler-app-sso-entra).
The exact steps for configuring an IdP vary depending on the provider (Okta, Azure AD, etc.). Please refer to the IdP's documentation for instructions on creating a SAML application. For SSO integration with Azure AD / Entra ID, see our [Entra ID configuration instructions](/user-guide/tutorials/prowler-app-sso-entra).
</Info>
</Info>
#### Step 3B: Configure Prowler from App Catalog - Okta
</Tab>
<Tab title="Okta App Catalog">
Instead of creating a custom SAML integration, Okta administrators can configure Prowler Cloud directly from Okta's application catalog.
Instead of creating a custom SAML integration, Okta administrators can configure Prowler Cloud directly from Okta's application catalog:
You can find a walkthrough video [here](https://youtu.be/NjSp5owvCdY).
1. **Access App Catalog**: Navigate to the IdP's application catalog (e.g., [Browse App Catalog](https://www.okta.com/integrations/) in Okta).
1. **Access App Catalog**: Navigate to the IdP's application catalog (e.g., [Browse App Catalog](https://www.okta.com/integrations/) in Okta).
![Browse App Catalog](/images/prowler-app/saml/app-catalog-browse.png)
![Browse App Catalog](/images/prowler-app/saml/app-catalog-browse.png)
2. **Search for Prowler Cloud**: Use the search functionality to find "Prowler Cloud" in the app catalog. The official Prowler Cloud application will appear in the search results.
2. **Search for Prowler Cloud**: Use the search functionality to find "Prowler Cloud" in the app catalog. The official Prowler Cloud application will appear in the search results.
![Search for Prowler](/images/prowler-app/saml/app-catalog-browse-prowler.png)
![Search for Prowler](/images/prowler-app/saml/app-catalog-browse-prowler.png)
3. **Select Prowler Cloud Application**: Click on the Prowler Cloud application from the search results to view its details page.
3. **Select Prowler Cloud Application**: Click the Prowler Cloud application from the search results to view its details page.
![Prowler Application Details](/images/prowler-app/saml/app-catalog-browse-prowler-add.png)
![Prowler Application Details](/images/prowler-app/saml/app-catalog-browse-prowler-add.png)
4. **Add Integration**: Click the "Add Integration" button to begin adding Prowler Cloud to the organization's applications.
4. **Add Integration**: Click the "Add Integration" button to begin adding Prowler Cloud to the organization's applications.
5. **Configure General Settings**: In the "Add Prowler Cloud" configuration screen, the integration automatically configures the necessary settings.
5. **Configure General Settings**: In the "Add Prowler Cloud" configuration screen, the integration automatically configures the necessary settings.
![Add Prowler Configuration](/images/prowler-app/saml/app-catalog-browse-prowler-configure.png)
![Add Prowler Configuration](/images/prowler-app/saml/app-catalog-browse-prowler-configure.png)
6. **Assign Users**: Navigate to the **Assignments** tab and assign the appropriate users or groups to the Prowler application by clicking "Assign" and selecting "Assign to People" or "Assign to Groups".
6. **Assign Users**: Navigate to the "Assignments" tab and assign the appropriate users or groups to the Prowler application by clicking "Assign" and selecting "Assign to People" or "Assign to Groups".
With this step, the Okta app catalog configuration is complete. Users can now access Prowler Cloud using either [IdP-initiated](#idp-initiated-sso) or [SP-initiated SSO](#sp-initiated-sso) flows.
With this step, the Okta app catalog configuration is complete. Users can now access Prowler Cloud using either [IdP-initiated](#idp-initiated-sso) or [SP-initiated SSO](#sp-initiated-sso) flows.
**If you used Step 3B (Okta App Catalog)**, jump to [Step 6: Save and Verify Configuration](#step-6-save-and-verify-configuration).
7. **Download Metadata XML**: Inside the "Sign On" section, go to the "Metadata URL" and download the metadata XML file.
Jump to [Step 5: Upload IdP Metadata to Prowler](#step-5:-upload-idp-metadata-to-prowler).
</Tab>
</Tabs>
#### Step 4: Configure Attribute Mapping in the IdP
@@ -120,7 +127,7 @@ For Prowler App to correctly identify and provision users, configure the IdP to
<Info>
**IdP Attribute Mapping**
Note that the attribute name is just an example and may be different depending on the IdP. For instance, if the IdP provides a 'division' attribute, it can be mapped to 'userType'.
Note that the attribute name is just an example and may be different depending on the IdP. For instance, if the IdP provides a `division` attribute, it can be mapped to `userType`.
![IdP configuration](/images/prowler-app/saml/saml_attribute_statements.png)
</Info>
@@ -156,7 +163,8 @@ Click the "Save" button to complete the setup. The "SAML Integration" card will
The exact steps for configuring an IdP vary depending on the provider (Okta, Azure AD, etc.). Please refer to the IdP's documentation for instructions on creating a SAML application.
</Info>
##### Remove SAML Configuration
### Remove SAML Configuration
SAML SSO can be disabled by removing the existing configuration from the integration panel.
![Remove SAML configuration](/images/prowler-app/saml/saml-step-remove.png)
+26 -199
View File
@@ -77,208 +77,35 @@ Steps to add a provider:
## **Step 4: Configure the Provider**
Select the cloud provider you want to scan.
Select the cloud provider to scan and configure authentication credentials. Each provider has specific requirements and authentication methods.
<img src="/images/select-provider.png" alt="Select a Provider" width="700" />
Once chosen, enter the Provider UID for authentication:
- **AWS**: Enter your AWS Account ID.
- **GCP**: Enter your GCP Project ID.
- **Azure**: Enter your Azure Subscription ID.
- **Kubernetes**: Enter your Kubernetes Cluster context of your kubeconfig file.
- **M365**: Enter your M365 Domain ID.
Optionally, provide a **Provider Alias** for easier identification. Follow the instructions provided to add your credentials:
### **Step 4.1: AWS Credentials**
For AWS, enter your `AWS Account ID` and choose one of the following methods to connect:
#### **Step 4.1.1: IAM Access Keys**
1. Select `Connect via Credentials`.
<img src="/images/connect-aws-credentials.png" alt="AWS Credentials" width="350" />
2. Enter your `Access Key ID`, `Secret Access Key` and optionally a `Session Token`:
<img src="/images/aws-credentials.png" alt="AWS Credentials" width="350" />
#### **Step 4.1.2: IAM Role**
1. Select `Connect assuming IAM Role`.
<img src="/images/connect-aws-role.png" alt="AWS Role" width="350" />
2. Enter the `Role ARN` and any optional field like the AWS Access Keys to assume the role, the `External ID`, the `Role Session Name` or the `Session Duration`:
<img src="/images/aws-role.png" alt="AWS Role" width="700" />
<Note>
Check if your AWS Security Token Service (STS) has the EU (Ireland) endpoint active. If not, we will not be able to connect to your AWS account.
If that is the case your STS configuration may look like this:
<img src="/images/sts-configuration.png" alt="AWS Role" width="800" />
To solve this issue, please activate the EU (Ireland) STS endpoint.
</Note>
### **Step 4.2: Azure Credentials**:
For Azure, Prowler App uses a service principal application to authenticate. For more information about the process of creating and adding permissions to a service principal refer to this [section](/user-guide/providers/azure/authentication). When you finish creating and adding the [Entra](/user-guide/providers/azure/create-prowler-service-principal#assigning-proper-permissions) and [Subscription](/user-guide/providers/azure/subscriptions) scope permissions to the service principal, enter the `Tenant ID`, `Client ID` and `Client Secret` of the service principal application.
<img src="/images/azure-credentials.png" alt="Azure Credentials" width="700" />
---
### **Step 4.3: GCP Credentials**
For Google Cloud, first enter your `GCP Project ID` and then select the authentication method you want to use:
- **Service Account Authentication** (**Recommended**)
- **Application Default Credentials**
**Service Account Authentication** is the recommended authentication method for automated systems and machine-to-machine interactions, like Prowler. For detailed information about this, refer to the [Google Cloud documentation](https://cloud.google.com/iam/docs/service-account-overview).
<img src="/images/prowler-app/gcp-auth-methods.png" alt="GCP Authentication Methods" width="700" />
#### **Step 4.3.1: Service Account Authentication**
First of all, in the same project that you selected in the previous step, you need to create a service account and then generate a key in JSON format for it. For more information about this, you can follow the next Google Cloud documentation tutorials:
- [Create a service account](https://cloud.google.com/iam/docs/creating-managing-service-accounts)
- [Generate a key for a service account](https://cloud.google.com/iam/docs/creating-managing-service-account-keys)
<img src="/images/prowler-app/gcp-service-account-creds.png" alt="GCP Service Account Credentials" width="700" />
#### **Step 4.3.2: Application Default Credentials**
1. Run the following command in your terminal to authenticate with GCP:
```bash
gcloud auth application-default login
```
2. Once authenticated, get the `Client ID`, `Client Secret` and `Refresh Token` from `~/.config/gcloud/application_default_credentials`.
3. Paste the `Client ID`, `Client Secret` and `Refresh Token` into Prowler App.
<img src="/images/gcp-credentials.png" alt="GCP Credentials" width="700" />
### **Step 4.4: Kubernetes Credentials**:
For Kubernetes, Prowler App uses a `kubeconfig` file to authenticate, paste the contents of your `kubeconfig` file into the `Kubeconfig content` field.
By default, the `kubeconfig` file is located at `~/.kube/config`.
<img src="/images/kubernetes-credentials.png" alt="Kubernetes Credentials" width="700" />
If you are adding an **EKS**, **GKE**, **AKS** or external cluster, follow these additional steps to ensure proper authentication:
**Make sure your cluster allow traffic from the Prowler Cloud IP address `52.48.254.174/32`**
1. Apply the necessary Kubernetes resources to your EKS, GKE, AKS or external cluster (you can find the files in the [`kubernetes` directory of the Prowler repository](https://github.com/prowler-cloud/prowler/tree/master/kubernetes)):
```console
kubectl apply -f kubernetes/prowler-sa.yaml
kubectl apply -f kubernetes/prowler-role.yaml
kubectl apply -f kubernetes/prowler-rolebinding.yaml
```
2. Generate a long-lived token for authentication:
```console
kubectl create token prowler-sa -n prowler-ns --duration=0
```
- **Security Note:** The `--duration=0` option generates a non-expiring token, which may pose a security risk if not managed properly. Users should decide on an appropriate expiration time based on their security policies. If a limited-time token is preferred, set `--duration=<TIME>` (e.g., `--duration=24h`).
- **Important:** If the token expires, Prowler Cloud will no longer be able to authenticate with the cluster. In this case, you will need to generate a new token and **remove and re-add the provider in Prowler Cloud** with the updated `kubeconfig`.
3. Update your `kubeconfig` to use the ServiceAccount token:
```console
kubectl config set-credentials prowler-sa --token=<SA_TOKEN>
kubectl config set-context <CONTEXT_NAME> --user=prowler-sa
```
Replace `<SA_TOKEN>` with the generated token and `<CONTEXT_NAME>` with your KubeConfig Context Name of your EKS, GKE or AKS cluster.
4. Now you can add the modified `kubeconfig` in Prowler Cloud. Then test the connection.
### **Step 4.5: M365 Credentials**
Enter your Microsoft Entra domain (primary tenant domain) and select how the provider should authenticate. Prowler App guides you through the process:
<img src="/images/providers/m365-auth-selection-form.png" alt="M365 authentication method selection" width="700" />
- **Application Client Secret Authentication**: Client secret-based authentication.
- **Application Certificate Authentication (Recommended)**: Certificate-based authentication. Recommended by Microsoft.
#### Step 4.5.1: Application Client Secret Authentication
1. **Enter your tenant ID**: This is the unique identifier for your Microsoft Entra ID directory.
2. **Enter your application (client) ID**: This is the unique identifier assigned to your app registration in Microsoft Entra ID.
3. **Enter your client secret**: This is the secret key used to authenticate your application.
<img src="/images/providers/secret-form.png" alt="M365 client secret authentication form" width="700" />
For full setup instructions, certificate generation commands, and required permissions, review the [Microsoft 365 provider requirements](/user-guide/providers/microsoft365/getting-started-m365).
#### Step 4.5.2: Application Certificate Authentication (Recommended)
1. **Enter your tenant ID**: This is the unique identifier for your Microsoft Entra ID directory.
2. **Enter your application (client) ID**: This is the unique identifier assigned to your app registration in Microsoft Entra ID.
3. **Upload your certificate file content**: This is the **Base64** encoded certificate content used to authenticate your application.
<img src="/images/providers/certificate-form.png" alt="M365 certificate authentication form" width="700" />
### **Step 4.6: GitHub Credentials**
For GitHub, you must enter your Provider ID (username or organization name) and choose the authentication method you want to use:
- **Personal Access Token** (Recommended for individual users)
- **OAuth App Token** (For applications requiring user consent)
- **GitHub App** (Recommended for organizations and production use)
<Note>
For full setup instructions and requirements, check the [GitHub provider requirements](/user-guide/providers/github/getting-started-github).
</Note>
<img src="/images/prowler-app/github-auth-methods.png" alt="GitHub Authentication Methods" width="700" />
#### **Step 4.6.1: Personal Access Token**
Personal Access Tokens provide the simplest GitHub authentication method and support individual user authentication or testing scenarios.
- Select `Personal Access Token` and enter your `Personal Access Token`:
<img src="/images/prowler-app/github-pat-credentials.png" alt="GitHub Personal Access Token Credentials" width="700" />
<Note>
For detailed instructions on creating a Personal Access Token and the exact permissions required, check the [GitHub Personal Access Token tutorial](/user-guide/providers/github/getting-started-github#1-personal-access-token-pat).
</Note>
#### **Step 4.6.2: OAuth App Token**
OAuth Apps enable applications to act on behalf of users with explicit consent.
- Select `OAuth App Token` and enter your `OAuth App Token`:
<img src="/images/prowler-app/github-oauth-credentials.png" alt="GitHub OAuth App Credentials" width="700" />
<Note>
To create an OAuth App, go to GitHub Settings → Developer settings → OAuth Apps → New OAuth App. You'll need to exchange an authorization code for an access token using the OAuth flow.
</Note>
#### **Step 4.6.3: GitHub App**
GitHub Apps provide the recommended integration method for accessing multiple repositories or organizations.
- Select `GitHub App` and enter your `GitHub App ID` and `GitHub App Private Key`:
<img src="/images/prowler-app/github-app-credentials.png" alt="GitHub App Credentials" width="700" />
<Note>
To create a GitHub App, go to GitHub Settings → Developer settings → GitHub Apps → New GitHub App. Configure the necessary permissions and generate a private key. Install the app to your account or organization and provide the App ID and private key content.
</Note>
For detailed instructions on configuring credentials for each provider, refer to the provider-specific getting started guides:
<Columns cols={3}>
<Card title="AWS" icon="aws" href="/user-guide/providers/aws/getting-started-aws">
Configure AWS authentication using IAM Access Keys or Assumed Role credentials.
</Card>
<Card title="Azure" icon="microsoft" href="/user-guide/providers/azure/getting-started-azure">
Set up Azure authentication using Service Principal credentials.
</Card>
<Card title="Google Cloud" icon="google" href="/user-guide/providers/gcp/getting-started-gcp">
Configure GCP authentication with Service Account or Application Default Credentials.
</Card>
<Card title="Kubernetes" icon="cloud" href="/user-guide/providers/kubernetes/getting-started-k8s">
Set up Kubernetes authentication using kubeconfig files for cluster access.
</Card>
<Card title="Microsoft 365" icon="microsoft" href="/user-guide/providers/microsoft365/getting-started-m365">
Configure M365 authentication with Application Certificate or Client Secret.
</Card>
<Card title="GitHub" icon="github" href="/user-guide/providers/github/getting-started-github">
Set up GitHub authentication using Personal Access Token, OAuth App, or GitHub App.
</Card>
<Card title="Infrastructure as Code" icon="code" href="/user-guide/providers/iac/getting-started-iac">
Scan IaC public or private repositories for security issues.
</Card>
</Columns>
## **Step 5: Test Connection**
After adding your credentials of your cloud account, click the `Launch` button to verify that Prowler App can successfully connect to your provider:
@@ -0,0 +1,29 @@
---
title: 'Prowler Cloud Public IPs'
---
## Overview
Prowler Cloud uses a dedicated egress IPv4 address for all outbound connections to customer infrastructure. This enables organizations to implement network-level security controls by whitelisting Prowler's IP address.
## Use Cases
Whitelisting Prowler's egress IP address enables:
- **Credential Usage Control**: Restrict where cloud provider credentials can be used from across AWS, Azure, GCP, and other providers
- **Kubernetes Security**: Limit inbound HTTPS traffic to clusters by allowing only Prowler's IP address
- **Compliance Requirements**: Meet security policies requiring allowlisting of external services
## Query the Egress IP Address
Retrieve Prowler Cloud's current egress IP address using the following command:
```bash
dig egress.prowler.com +short
```
This command returns the IPv4 address that Prowler Cloud uses for all outbound connections to customer infrastructure.
<Note>
The egress IP address is stable, but it is recommended to periodically verify it remains current by querying `egress.prowler.com`.
</Note>
+5
View File
@@ -2,6 +2,11 @@
All notable changes to the **Prowler MCP Server** are documented in this file.
## [0.1.1] (Prowler UNRELEASED)
### Fixed
- Fix documentation MCP Server to return list of dictionaries [(#9205)](https://github.com/prowler-cloud/prowler/pull/9205)
## [0.1.0] (Prowler 5.13.0)
### Added
@@ -1,9 +1,8 @@
from typing import List
from typing import Any, List
from fastmcp import FastMCP
from prowler_mcp_server.prowler_documentation.search_engine import (
ProwlerDocsSearchEngine,
SearchResult,
)
# Initialize FastMCP server
@@ -15,7 +14,7 @@ prowler_docs_search_engine = ProwlerDocsSearchEngine()
def search(
query: str,
page_size: int = 5,
) -> List[SearchResult]:
) -> List[dict[str, Any]]:
"""
Search in Prowler documentation.
Generated
+20 -9
View File
@@ -621,6 +621,24 @@ azure-mgmt-core = ">=1.3.2"
isodate = ">=0.6.1"
typing-extensions = ">=4.6.0"
[[package]]
name = "azure-mgmt-postgresqlflexibleservers"
version = "1.1.0"
description = "Microsoft Azure Postgresqlflexibleservers Management Client Library for Python"
optional = false
python-versions = ">=3.8"
groups = ["main"]
files = [
{file = "azure_mgmt_postgresqlflexibleservers-1.1.0-py3-none-any.whl", hash = "sha256:87ddb5a5e6d12c45769485d234cfe0322140e3a0a7636d0e61fb00ac544b5d20"},
{file = "azure_mgmt_postgresqlflexibleservers-1.1.0.tar.gz", hash = "sha256:9ede9d8ba63e9d2879cb74adc903c649af3bc5460a02787287b0cd18d754af14"},
]
[package.dependencies]
azure-common = ">=1.1"
azure-mgmt-core = ">=1.3.2"
isodate = ">=0.6.1"
typing-extensions = ">=4.6.0"
[[package]]
name = "azure-mgmt-rdbms"
version = "10.1.0"
@@ -2348,8 +2366,6 @@ python-versions = "*"
groups = ["dev"]
files = [
{file = "jsonpath-ng-1.7.0.tar.gz", hash = "sha256:f6f5f7fd4e5ff79c785f1573b394043b39849fb2bb47bcead935d12b00beab3c"},
{file = "jsonpath_ng-1.7.0-py2-none-any.whl", hash = "sha256:898c93fc173f0c336784a3fa63d7434297544b7198124a68f9a3ef9597b0ae6e"},
{file = "jsonpath_ng-1.7.0-py3-none-any.whl", hash = "sha256:f3d7f9e848cba1b6da28c55b1c26ff915dc9e0b1ba7e752a53d6da8d5cbd00b6"},
]
[package.dependencies]
@@ -4424,7 +4440,7 @@ version = "2025.9.18"
description = "Alternative regular expression module, to replace re."
optional = false
python-versions = ">=3.9"
groups = ["dev", "docs"]
groups = ["dev"]
files = [
{file = "regex-2025.9.18-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:12296202480c201c98a84aecc4d210592b2f55e200a1d193235c4db92b9f6788"},
{file = "regex-2025.9.18-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:220381f1464a581f2ea988f2220cf2a67927adcef107d47d6897ba5a2f6d51a4"},
@@ -4868,7 +4884,6 @@ files = [
{file = "ruamel.yaml.clib-0.2.12-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f66efbc1caa63c088dead1c4170d148eabc9b80d95fb75b6c92ac0aad2437d76"},
{file = "ruamel.yaml.clib-0.2.12-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:22353049ba4181685023b25b5b51a574bce33e7f51c759371a7422dcae5402a6"},
{file = "ruamel.yaml.clib-0.2.12-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:932205970b9f9991b34f55136be327501903f7c66830e9760a8ffb15b07f05cd"},
{file = "ruamel.yaml.clib-0.2.12-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a52d48f4e7bf9005e8f0a89209bf9a73f7190ddf0489eee5eb51377385f59f2a"},
{file = "ruamel.yaml.clib-0.2.12-cp310-cp310-win32.whl", hash = "sha256:3eac5a91891ceb88138c113f9db04f3cebdae277f5d44eaa3651a4f573e6a5da"},
{file = "ruamel.yaml.clib-0.2.12-cp310-cp310-win_amd64.whl", hash = "sha256:ab007f2f5a87bd08ab1499bdf96f3d5c6ad4dcfa364884cb4549aa0154b13a28"},
{file = "ruamel.yaml.clib-0.2.12-cp311-cp311-macosx_13_0_arm64.whl", hash = "sha256:4a6679521a58256a90b0d89e03992c15144c5f3858f40d7c18886023d7943db6"},
@@ -4877,7 +4892,6 @@ files = [
{file = "ruamel.yaml.clib-0.2.12-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:811ea1594b8a0fb466172c384267a4e5e367298af6b228931f273b111f17ef52"},
{file = "ruamel.yaml.clib-0.2.12-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:cf12567a7b565cbf65d438dec6cfbe2917d3c1bdddfce84a9930b7d35ea59642"},
{file = "ruamel.yaml.clib-0.2.12-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:7dd5adc8b930b12c8fc5b99e2d535a09889941aa0d0bd06f4749e9a9397c71d2"},
{file = "ruamel.yaml.clib-0.2.12-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1492a6051dab8d912fc2adeef0e8c72216b24d57bd896ea607cb90bb0c4981d3"},
{file = "ruamel.yaml.clib-0.2.12-cp311-cp311-win32.whl", hash = "sha256:bd0a08f0bab19093c54e18a14a10b4322e1eacc5217056f3c063bd2f59853ce4"},
{file = "ruamel.yaml.clib-0.2.12-cp311-cp311-win_amd64.whl", hash = "sha256:a274fb2cb086c7a3dea4322ec27f4cb5cc4b6298adb583ab0e211a4682f241eb"},
{file = "ruamel.yaml.clib-0.2.12-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:20b0f8dc160ba83b6dcc0e256846e1a02d044e13f7ea74a3d1d56ede4e48c632"},
@@ -4886,7 +4900,6 @@ files = [
{file = "ruamel.yaml.clib-0.2.12-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:749c16fcc4a2b09f28843cda5a193e0283e47454b63ec4b81eaa2242f50e4ccd"},
{file = "ruamel.yaml.clib-0.2.12-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:bf165fef1f223beae7333275156ab2022cffe255dcc51c27f066b4370da81e31"},
{file = "ruamel.yaml.clib-0.2.12-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:32621c177bbf782ca5a18ba4d7af0f1082a3f6e517ac2a18b3974d4edf349680"},
{file = "ruamel.yaml.clib-0.2.12-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:b82a7c94a498853aa0b272fd5bc67f29008da798d4f93a2f9f289feb8426a58d"},
{file = "ruamel.yaml.clib-0.2.12-cp312-cp312-win32.whl", hash = "sha256:e8c4ebfcfd57177b572e2040777b8abc537cdef58a2120e830124946aa9b42c5"},
{file = "ruamel.yaml.clib-0.2.12-cp312-cp312-win_amd64.whl", hash = "sha256:0467c5965282c62203273b838ae77c0d29d7638c8a4e3a1c8bdd3602c10904e4"},
{file = "ruamel.yaml.clib-0.2.12-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:4c8c5d82f50bb53986a5e02d1b3092b03622c02c2eb78e29bec33fd9593bae1a"},
@@ -4895,7 +4908,6 @@ files = [
{file = "ruamel.yaml.clib-0.2.12-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:96777d473c05ee3e5e3c3e999f5d23c6f4ec5b0c38c098b3a5229085f74236c6"},
{file = "ruamel.yaml.clib-0.2.12-cp313-cp313-musllinux_1_1_i686.whl", hash = "sha256:3bc2a80e6420ca8b7d3590791e2dfc709c88ab9152c00eeb511c9875ce5778bf"},
{file = "ruamel.yaml.clib-0.2.12-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:e188d2699864c11c36cdfdada94d781fd5d6b0071cd9c427bceb08ad3d7c70e1"},
{file = "ruamel.yaml.clib-0.2.12-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4f6f3eac23941b32afccc23081e1f50612bdbe4e982012ef4f5797986828cd01"},
{file = "ruamel.yaml.clib-0.2.12-cp313-cp313-win32.whl", hash = "sha256:6442cb36270b3afb1b4951f060eccca1ce49f3d087ca1ca4563a6eb479cb3de6"},
{file = "ruamel.yaml.clib-0.2.12-cp313-cp313-win_amd64.whl", hash = "sha256:e5b8daf27af0b90da7bb903a876477a9e6d7270be6146906b276605997c7e9a3"},
{file = "ruamel.yaml.clib-0.2.12-cp39-cp39-macosx_12_0_arm64.whl", hash = "sha256:fc4b630cd3fa2cf7fce38afa91d7cfe844a9f75d7f0f36393fa98815e911d987"},
@@ -4904,7 +4916,6 @@ files = [
{file = "ruamel.yaml.clib-0.2.12-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e2f1c3765db32be59d18ab3953f43ab62a761327aafc1594a2a1fbe038b8b8a7"},
{file = "ruamel.yaml.clib-0.2.12-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:d85252669dc32f98ebcd5d36768f5d4faeaeaa2d655ac0473be490ecdae3c285"},
{file = "ruamel.yaml.clib-0.2.12-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:e143ada795c341b56de9418c58d028989093ee611aa27ffb9b7f609c00d813ed"},
{file = "ruamel.yaml.clib-0.2.12-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:2c59aa6170b990d8d2719323e628aaf36f3bfbc1c26279c0eeeb24d05d2d11c7"},
{file = "ruamel.yaml.clib-0.2.12-cp39-cp39-win32.whl", hash = "sha256:beffaed67936fbbeffd10966a4eb53c402fafd3d6833770516bf7314bc6ffa12"},
{file = "ruamel.yaml.clib-0.2.12-cp39-cp39-win_amd64.whl", hash = "sha256:040ae85536960525ea62868b642bdb0c2cc6021c9f9d507810c0c604e66f5a7b"},
{file = "ruamel.yaml.clib-0.2.12.tar.gz", hash = "sha256:6c8fbb13ec503f99a91901ab46e0b07ae7941cd527393187039aec586fdfd36f"},
@@ -5677,4 +5688,4 @@ type = ["pytest-mypy"]
[metadata]
lock-version = "2.1"
python-versions = ">3.9.1,<3.13"
content-hash = "ea79d82b4e255ec4604f440a507da6dac38b57af93356761ac793678aa615cf5"
content-hash = "a367e65bc43c0a16495a3d0f6eab8b356cc49b509e329b61c6641cd87f374ff4"
+26 -1
View File
@@ -11,6 +11,8 @@ All notable changes to the **Prowler SDK** are documented in this file.
- `cloudstorage_bucket_versioning_enabled` check for GCP provider [(#9014)](https://github.com/prowler-cloud/prowler/pull/9014)
- `cloudstorage_bucket_soft_delete_enabled` check for GCP provider [(#9028)](https://github.com/prowler-cloud/prowler/pull/9028)
- `cloudstorage_bucket_logging_enabled` check for GCP provider [(#9091)](https://github.com/prowler-cloud/prowler/pull/9091)
- `cloudstorage_audit_logs_enabled` check for GCP provider [(#9220)](https://github.com/prowler-cloud/prowler/pull/9220)
- `cloudstorage_bucket_sufficient_retention_period` check for GCP provider [(#9149)](https://github.com/prowler-cloud/prowler/pull/9149)
- C5 compliance framework for Azure provider [(#9081)](https://github.com/prowler-cloud/prowler/pull/9081)
- C5 compliance framework for the GCP provider [(#9097)](https://github.com/prowler-cloud/prowler/pull/9097)
- `organization_repository_creation_limited` check for GitHub provider [(#8844)](https://github.com/prowler-cloud/prowler/pull/8844)
@@ -19,6 +21,9 @@ All notable changes to the **Prowler SDK** are documented in this file.
- Add multiple compliance improvements [(#9145)](https://github.com/prowler-cloud/prowler/pull/9145)
- Added validation for invalid checks, services, and categories in `load_checks_to_execute` function [(#8971)](https://github.com/prowler-cloud/prowler/pull/8971)
- NIST CSF 2.0 compliance framework for the AWS provider [(#9185)](https://github.com/prowler-cloud/prowler/pull/9185)
- Add FedRAMP 20x KSI Low for AWS, Azure and GCP [(#9198)](https://github.com/prowler-cloud/prowler/pull/9198)
- Add verification for provider ID in MongoDB Atlas provider [(#9211)](https://github.com/prowler-cloud/prowler/pull/9211)
- Add `postgresql_flexible_server_entra_id_authentication_enabled` check for Azure provider [(#8764)](https://github.com/prowler-cloud/prowler/pull/8764)
### Changed
- Update AWS Direct Connect service metadata to new format [(#8855)](https://github.com/prowler-cloud/prowler/pull/8855)
@@ -29,6 +34,9 @@ All notable changes to the **Prowler SDK** are documented in this file.
- Update AWS EKS service metadata to new format [(#8890)](https://github.com/prowler-cloud/prowler/pull/8890)
- Update AWS Elastic Beanstalk service metadata to new format [(#8934)](https://github.com/prowler-cloud/prowler/pull/8934)
- Update AWS ElastiCache service metadata to new format [(#8933)](https://github.com/prowler-cloud/prowler/pull/8933)
- Update Kubernetes etcd service metadata to new format [(#9096)](https://github.com/prowler-cloud/prowler/pull/9096)
- Update MongoDB Atlas projects service metadata to new format [(#9093)](https://github.com/prowler-cloud/prowler/pull/9093)
- Update GitHub Organization service metadata to new format [(#9094)](https://github.com/prowler-cloud/prowler/pull/9094)
- Update AWS CodeBuild service metadata to new format [(#8851)](https://github.com/prowler-cloud/prowler/pull/8851)
- Update GCP Artifact Registry service metadata to new format [(#9088)](https://github.com/prowler-cloud/prowler/pull/9088)
- Update AWS EFS service metadata to new format [(#8889)](https://github.com/prowler-cloud/prowler/pull/8889)
@@ -38,9 +46,18 @@ All notable changes to the **Prowler SDK** are documented in this file.
- Update AWS FSx service metadata to new format [(#9006)](https://github.com/prowler-cloud/prowler/pull/9006)
- Update AWS Glacier service metadata to new format [(#9007)](https://github.com/prowler-cloud/prowler/pull/9007)
- Update oraclecloud analytics service metadata to new format [(#9114)](https://github.com/prowler-cloud/prowler/pull/9114)
- Update AWS ELB service metadata to new format [(#8935)](https://github.com/prowler-cloud/prowler/pull/8935)
- Update AWS CodeArtifact service metadata to new format [(#8850)](https://github.com/prowler-cloud/prowler/pull/8850)
- Rename OCI provider to oraclecloud with oci alias [(#9126)](https://github.com/prowler-cloud/prowler/pull/9126)
- Remove unnecessary tests for M365_PowerShell module [(#9204)](https://github.com/prowler-cloud/prowler/pull/9204)
- Update AWS ELB v2 service metadata to new format [(#9001)](https://github.com/prowler-cloud/prowler/pull/9001)
- Update oraclecloud cloudguard service metadata to new format [(#9223)](https://github.com/prowler-cloud/prowler/pull/9223)
- Update oraclecloud blockstorage service metadata to new format [(#9222)](https://github.com/prowler-cloud/prowler/pull/9222)
- Update oraclecloud audit service metadata to new format [(#9221)](https://github.com/prowler-cloud/prowler/pull/9221)
- Raise ASFF output error for non-AWS providers [(#9225)](https://github.com/prowler-cloud/prowler/pull/9225)
- Update AWS ECR service metadata to new format [(#8872)](https://github.com/prowler-cloud/prowler/pull/8872)
- Update AWS ECS service metadata to new format [(#8888)](https://github.com/prowler-cloud/prowler/pull/8888)
- Update AWS DocumentDB service metadata to new format [(#8862)](https://github.com/prowler-cloud/prowler/pull/8862)
---
@@ -48,6 +65,13 @@ All notable changes to the **Prowler SDK** are documented in this file.
### Fixed
- Check `check_name` has no `resource_name` error for GCP provider [(#9169)](https://github.com/prowler-cloud/prowler/pull/9169)
- Depth Truncation and parsing error in PowerShell queries [(#9181)](https://github.com/prowler-cloud/prowler/pull/9181)
- False negative in `iam_role_cross_service_confused_deputy_prevention` check [(#9213)](https://github.com/prowler-cloud/prowler/pull/9213)
- Fix M365 Teams `--sp-env-auth` connection error and enhanced timeout logging [(#9191)](https://github.com/prowler-cloud/prowler/pull/9191)
- Rename `get_oci_assessment_summary` to `get_oraclecloud_assessment_summary` in HTML output [(#9200)](https://github.com/prowler-cloud/prowler/pull/9200)
- Fix Validation and other errors in Azure provider [(#8915)](https://github.com/prowler-cloud/prowler/pull/8915)
- Update documentation URLs from docs.prowler.cloud to docs.prowler.com [(#9240)](https://github.com/prowler-cloud/prowler/pull/9240)
- Fix file name parsing for checks on Windows [(#9268)](https://github.com/prowler-cloud/prowler/pull/9268)
---
@@ -110,6 +134,7 @@ All notable changes to the **Prowler SDK** are documented in this file.
- Update AWS CloudFront service metadata to new format [(#8829)](https://github.com/prowler-cloud/prowler/pull/8829)
- Deprecate user authentication for M365 provider [(#8865)](https://github.com/prowler-cloud/prowler/pull/8865)
### Fixed
- Fix SNS topics showing empty AWS_ResourceID in Quick Inventory output [(#8762)](https://github.com/prowler-cloud/prowler/issues/8762)
- Fix HTML Markdown output for long strings [(#8803)](https://github.com/prowler-cloud/prowler/pull/8803)
+1 -2
View File
@@ -430,7 +430,7 @@ def prowler():
else:
# Refactor(CLI)
logger.critical(
"Slack integration needs SLACK_API_TOKEN and SLACK_CHANNEL_NAME environment variables (see more in https://docs.prowler.cloud/en/latest/tutorials/integrations/#slack)."
"Slack integration needs SLACK_API_TOKEN and SLACK_CHANNEL_NAME environment variables (see more in https://docs.prowler.com/user-guide/cli/tutorials/integrations#configuration-of-the-integration-with-slack)."
)
sys.exit(1)
@@ -573,7 +573,6 @@ def prowler():
generated_outputs["compliance"].append(prowler_threatscore)
prowler_threatscore.batch_write_data_to_file()
elif compliance_name.startswith("ccc_"):
filename = (
f"{output_options.output_directory}/compliance/"
f"{output_options.output_filename}_{compliance_name}.csv"
@@ -0,0 +1,347 @@
{
"Framework": "FedRAMP-20x-KSI-Low",
"Name": "FedRAMP 20x Key Security Indicators (KSIs) - Low Impact Level v25.05C",
"Version": "25.05C",
"Provider": "AWS",
"Description": "FedRAMP 20x Key Security Indicators (KSIs) Low Impact Level represent core security indicators for cloud service providers, focusing on automation, continuous monitoring, and cloud-native security principles per FedRAMP 20x Phase One pilot requirements for Low impact systems.",
"Requirements": [
{
"Id": "ksi-cmt",
"Name": "KSI-CMT: Change Management",
"Description": "A secure cloud service provider will ensure that all system changes are properly documented and configuration baselines are updated accordingly",
"Attributes": [
{
"ItemId": "ksi-cmt",
"Section": "Change Management",
"Service": "aws"
}
],
"Checks": [
"cloudtrail_multi_region_enabled",
"cloudtrail_log_file_validation_enabled",
"cloudtrail_s3_dataevents_read_enabled",
"cloudtrail_s3_dataevents_write_enabled",
"cloudwatch_changes_to_network_acls_alarm_configured",
"cloudwatch_changes_to_network_gateways_alarm_configured",
"cloudwatch_changes_to_network_route_tables_alarm_configured",
"cloudwatch_changes_to_vpcs_alarm_configured",
"cloudwatch_log_metric_filter_and_alarm_for_aws_config_configuration_changes_enabled",
"cloudwatch_log_metric_filter_and_alarm_for_cloudtrail_configuration_changes_enabled",
"cloudwatch_log_metric_filter_aws_organizations_changes",
"cloudwatch_log_metric_filter_for_s3_bucket_policy_changes",
"cloudwatch_log_metric_filter_policy_changes",
"cloudwatch_log_metric_filter_security_group_changes",
"config_recorder_all_regions_enabled",
"ec2_instance_managed_by_ssm",
"ec2_instance_older_than_specific_days",
"ssm_managed_compliant_patching",
"ssm_managed_instance_compliance_association_compliant",
"ssm_managed_instance_compliance_patch_compliant"
]
},
{
"Id": "ksi-cna",
"Name": "KSI-CNA: Cloud Native Architecture",
"Description": "A secure cloud service offering will use cloud native architecture and design principles to enforce and enhance the Confidentiality, Integrity and Availability of the system",
"Attributes": [
{
"ItemId": "ksi-cna",
"Section": "Cloud Native Architecture",
"Service": "aws"
}
],
"Checks": [
"autoscaling_group_multiple_az",
"autoscaling_group_multiple_instance_types",
"autoscaling_group_capacity_rebalance_enabled",
"dynamodb_tables_pitr_enabled",
"dynamodb_tables_deletion_protection_enabled",
"ec2_instance_imdsv2_enabled",
"ec2_networkacl_allow_ingress_any_port",
"ec2_securitygroup_default_restrict_traffic",
"ec2_securitygroup_allow_ingress_from_internet_to_any_port",
"eks_cluster_network_policy_enabled",
"eks_cluster_not_publicly_accessible",
"eks_cluster_private_nodes_enabled",
"eks_cluster_uses_a_supported_version",
"elb_cross_zone_load_balancing_enabled",
"elbv2_alb_multi_az_scheme",
"elbv2_waf_acl_attached",
"rds_instance_multi_az",
"rds_cluster_multi_az",
"vpc_subnet_auto_assign_public_ip_disabled",
"vpc_default_security_group_restricts_traffic",
"vpc_peering_connection_routing_tables_with_least_privilege"
]
},
{
"Id": "ksi-iam",
"Name": "KSI-IAM: Identity and Access Management",
"Description": "A secure cloud service offering will protect user data, control access, and apply zero trust principles",
"Attributes": [
{
"ItemId": "ksi-iam",
"Section": "Identity and Access Management",
"Service": "aws"
}
],
"Checks": [
"iam_administrator_access_with_mfa",
"iam_aws_attached_policy_no_administrative_privileges",
"iam_customer_attached_policy_no_administrative_privileges",
"iam_inline_policy_no_administrative_privileges",
"iam_no_custom_policy_permissive_role_assumption",
"iam_no_root_access_key",
"iam_password_policy_expires_passwords_within_90_days_or_less",
"iam_password_policy_lowercase",
"iam_password_policy_minimum_length_14",
"iam_password_policy_number",
"iam_password_policy_reuse_24",
"iam_password_policy_symbol",
"iam_password_policy_uppercase",
"iam_policy_attached_only_to_group_or_roles",
"iam_policy_no_full_access_to_cloudtrail",
"iam_policy_no_full_access_to_kms",
"iam_root_hardware_mfa_enabled",
"iam_root_mfa_enabled",
"iam_rotate_access_key_90_days",
"iam_user_accesskey_unused",
"iam_user_console_access_unused",
"iam_user_hardware_mfa_enabled",
"iam_user_mfa_enabled_console_access",
"iam_user_two_active_access_key",
"organizations_scp_check_deny_regions",
"organizations_opt_out_ai_services_policy"
]
},
{
"Id": "ksi-inr",
"Name": "KSI-INR: Incident Response",
"Description": "A secure cloud service offering will respond to incidents according to FedRAMP requirements and cloud service provider policies",
"Attributes": [
{
"ItemId": "ksi-inr",
"Section": "Incident Response",
"Service": "aws"
}
],
"Checks": [
"guardduty_centrally_managed",
"guardduty_ec2_malware_protection_enabled",
"guardduty_eks_audit_log_enabled",
"guardduty_eks_protection_enabled",
"guardduty_eks_runtime_monitoring_enabled",
"guardduty_is_enabled",
"guardduty_lambda_protection_enabled",
"guardduty_malware_protection_enabled",
"guardduty_no_high_severity_findings",
"guardduty_rds_protection_enabled",
"guardduty_s3_protection_enabled",
"inspector2_is_enabled",
"inspector2_active_findings_exist",
"securityhub_enabled",
"sns_topics_kms_encryption_at_rest_enabled"
]
},
{
"Id": "ksi-mla",
"Name": "KSI-MLA: Monitoring, Logging, and Auditing",
"Description": "A secure cloud service offering will monitor, log, and audit all important events, activity, and changes",
"Attributes": [
{
"ItemId": "ksi-mla",
"Section": "Monitoring, Logging, and Auditing",
"Service": "aws"
}
],
"Checks": [
"apigateway_restapi_logging_enabled",
"cloudtrail_cloudwatch_logging_enabled",
"cloudtrail_kms_encryption_enabled",
"cloudtrail_log_file_validation_enabled",
"cloudtrail_multi_region_enabled",
"cloudtrail_s3_dataevents_read_enabled",
"cloudtrail_s3_dataevents_write_enabled",
"cloudwatch_log_group_kms_encryption_enabled",
"cloudwatch_log_group_retention_policy_specific_days_enabled",
"ecs_cluster_container_insights_enabled",
"eks_cluster_control_plane_audit_logging_enabled",
"elb_logging_enabled",
"elbv2_logging_enabled",
"inspector2_is_enabled",
"opensearch_service_domains_cloudwatch_logging_enabled",
"rds_instance_enhanced_monitoring_enabled",
"rds_instance_integration_cloudwatch_logs",
"redshift_cluster_audit_logging",
"s3_bucket_server_access_logging_enabled",
"vpc_flow_logs_enabled",
"wafv2_webacl_logging_enabled"
]
},
{
"Id": "ksi-piy",
"Name": "KSI-PIY: Policy and Inventory",
"Description": "A secure cloud service offering will have intentional, organized, universal guidance for how every information resource, including personnel, is secured",
"Attributes": [
{
"ItemId": "ksi-piy",
"Section": "Policy and Inventory",
"Service": "aws"
}
],
"Checks": [
"config_recorder_all_regions_enabled",
"config_recorder_using_aws_service_role",
"ec2_instance_managed_by_ssm",
"organizations_account_part_of_organizations",
"organizations_delegated_administrators",
"organizations_scp_check_deny_regions",
"organizations_tags_policies_enabled_and_attached",
"resourceexplorer_indexes_found",
"ssm_managed_instance_compliance_association_compliant",
"trustedadvisor_premium_support_plan_subscribed"
]
},
{
"Id": "ksi-rpl",
"Name": "KSI-RPL: Recovery Planning",
"Description": "A secure cloud service offering will define, maintain, and test incident response plan(s) and recovery capabilities to ensure minimal service disruption and data loss",
"Attributes": [
{
"ItemId": "ksi-rpl",
"Section": "Recovery Planning",
"Service": "aws"
}
],
"Checks": [
"backup_plans_exist",
"backup_reportplans_exist",
"backup_vaults_exist",
"backup_vaults_encrypted",
"backup_recovery_point_encrypted",
"backup_recovery_point_manual_deletion_disabled",
"backup_recovery_point_minimum_retention_days",
"dlm_ebs_snapshot_lifecycle_policy_exists",
"dynamodb_tables_pitr_enabled",
"dynamodb_tables_deletion_protection_enabled",
"efs_have_backup_enabled",
"fsx_file_system_copy_tags_to_backups",
"rds_instance_backup_enabled",
"rds_instance_backup_retention_policy",
"rds_instance_deletion_protection",
"rds_cluster_deletion_protection",
"rds_snapshots_encrypted",
"redshift_cluster_automated_snapshot"
]
},
{
"Id": "ksi-svc",
"Name": "KSI-SVC: Service Configuration",
"Description": "A secure cloud service offering will follow FedRAMP encryption policies, continuously verify information resource integrity, and restrict access to third-party information resources",
"Attributes": [
{
"ItemId": "ksi-svc",
"Section": "Service Configuration",
"Service": "aws"
}
],
"Checks": [
"acm_certificates_expiration_check",
"apigateway_restapi_cache_encrypted",
"cloudtrail_kms_encryption_enabled",
"dax_cluster_encryption_enabled",
"dynamodb_table_encryption_enabled",
"dynamodb_table_encryption_uses_cmks",
"ebs_volume_encryption_enabled",
"ec2_ebs_default_encryption",
"ec2_instance_ebs_optimized",
"efs_encryption_at_rest_enabled",
"eks_cluster_envelope_encryption_enabled",
"elasticache_redis_cluster_encryption_at_rest_enabled",
"elasticache_redis_cluster_encryption_at_transit_enabled",
"elbv2_ssl_listeners",
"fsx_file_system_encryption_at_rest_enabled",
"kinesis_stream_encrypted_at_rest",
"kms_cmk_rotation_enabled",
"kms_cmk_not_scheduled_for_deletion",
"kms_key_not_publicly_accessible",
"rds_instance_storage_encrypted",
"rds_instance_storage_encrypted_with_cmk",
"rds_cluster_storage_encrypted",
"redshift_cluster_encryption_at_rest",
"redshift_cluster_encryption_in_transit",
"s3_bucket_server_side_encryption_enabled",
"s3_bucket_default_encryption",
"s3_bucket_secure_transport_policy",
"sagemaker_notebook_instance_encryption_enabled",
"sns_topics_kms_encryption_at_rest_enabled",
"sqs_queue_server_side_encryption_enabled"
]
},
{
"Id": "ksi-tpr",
"Name": "KSI-TPR: Third-Party Information Resources",
"Description": "A secure cloud service offering will understand, monitor, and manage supply chain risks from third-party information resources",
"Attributes": [
{
"ItemId": "ksi-tpr",
"Section": "Third-Party Information Resources",
"Service": "aws"
}
],
"Checks": [
"ecr_registry_scan_images_on_push_enabled",
"ecr_repositories_lifecycle_policy_enabled",
"ecr_repositories_not_publicly_accessible",
"ecr_repositories_scan_on_push_enabled",
"ecr_repositories_scan_vulnerabilities_in_latest_image",
"ecr_repositories_tag_immutability",
"inspector2_active_findings_exist",
"inspector2_is_enabled",
"awslambda_function_using_supported_runtimes",
"ssm_managed_compliant_patching",
"trustedadvisor_premium_support_plan_subscribed",
"guardduty_no_high_severity_findings"
]
},
{
"Id": "ksi-iam-07",
"Name": "KSI-IAM-07: Account Lifecycle Management",
"Description": "Securely manage the lifecycle and privileges of all accounts, roles, and groups",
"Attributes": [
{
"ItemId": "ksi-iam-07",
"Section": "Identity and Access Management",
"Service": "aws"
}
],
"Checks": [
"iam_no_root_access_key",
"iam_policy_attached_only_to_group_or_roles",
"iam_rotate_access_key_90_days",
"iam_user_accesskey_unused",
"iam_user_console_access_unused",
"organizations_delegated_administrators"
]
},
{
"Id": "ksi-mla-07",
"Name": "KSI-MLA-07: Monitoring and Logging Inventory",
"Description": "Maintain a list of information resources and event types that will be monitored, logged, and audited",
"Attributes": [
{
"ItemId": "ksi-mla-07",
"Section": "Monitoring, Logging, and Auditing",
"Service": "aws"
}
],
"Checks": [
"cloudtrail_multi_region_enabled",
"cloudwatch_log_group_retention_policy_specific_days_enabled",
"config_recorder_all_regions_enabled",
"inspector2_is_enabled",
"resourceexplorer_indexes_found"
]
}
]
}
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,358 @@
{
"Framework": "FedRAMP-20x-KSI-Low",
"Name": "FedRAMP 20x Key Security Indicators (KSIs) - Low Impact Level v25.05C",
"Version": "25.05C",
"Provider": "Azure",
"Description": "FedRAMP 20x Key Security Indicators (KSIs) Low Impact Level represent core security indicators for cloud service providers, focusing on automation, continuous monitoring, and cloud-native security principles per FedRAMP 20x Phase One pilot requirements for Low impact systems.",
"Requirements": [
{
"Id": "ksi-cmt",
"Name": "KSI-CMT: Change Management",
"Description": "A secure cloud service provider will ensure that all system changes are properly documented and configuration baselines are updated accordingly",
"Attributes": [
{
"ItemId": "ksi-cmt",
"Section": "Change Management",
"Service": "azure"
}
],
"Checks": [
"monitor_activity_log_alert_cmk_delete",
"monitor_activity_log_alert_create_policy_assignment",
"monitor_activity_log_alert_create_update_delete_network_sg",
"monitor_activity_log_alert_create_update_delete_network_sg_rule",
"monitor_activity_log_alert_create_update_delete_sql_server_fw_rule",
"monitor_activity_log_alert_create_update_nsg",
"monitor_activity_log_alert_create_update_public_ip_address",
"monitor_activity_log_alert_create_update_security_solution",
"monitor_activity_log_alert_delete_nsg",
"monitor_activity_log_alert_delete_policy_assignment",
"monitor_activity_log_alert_delete_public_ip_address",
"monitor_activity_log_alert_delete_security_solution",
"monitor_log_profile_all_categories",
"monitor_log_profile_all_regions",
"vm_agent_installed",
"vm_antimalware_solution_installed",
"vm_endpoint_protection_installed",
"vm_guest_configuration_installed",
"vm_guest_configuration_with_no_managed_identity",
"vm_guest_configuration_with_user_identity"
]
},
{
"Id": "ksi-cna",
"Name": "KSI-CNA: Cloud Native Architecture",
"Description": "A secure cloud service offering will use cloud native architecture and design principles to enforce and enhance the Confidentiality, Integrity and Availability of the system",
"Attributes": [
{
"ItemId": "ksi-cna",
"Section": "Cloud Native Architecture",
"Service": "azure"
}
],
"Checks": [
"aks_clusters_created_with_private_nodes",
"aks_clusters_public_access_disabled",
"aks_network_policy_enabled",
"app_function_vnet_integration_enabled",
"app_function_not_publicly_accessible",
"containerregistry_not_publicly_accessible",
"containerregistry_uses_private_link",
"cosmosdb_account_use_private_endpoints",
"cosmosdb_account_firewall_use_selected_networks",
"databricks_workspace_vnet_injection_enabled",
"keyvault_access_only_through_private_endpoints",
"keyvault_private_endpoints",
"network_bastion_host_exists",
"network_flow_logs_enabled",
"network_security_group_not_empty",
"network_sg_ssh_access_restricted",
"network_sg_rdp_access_restricted",
"network_sg_open_all_ports_to_any_source",
"network_watcher_enabled",
"postgresql_flexible_server_public_network_access_disabled",
"sqlserver_public_network_access_disabled",
"storage_default_network_access_rule_set_to_deny",
"vm_availability_zones_enabled",
"vm_availability_set_deployed"
]
},
{
"Id": "ksi-iam",
"Name": "KSI-IAM: Identity and Access Management",
"Description": "A secure cloud service offering will protect user data, control access, and apply zero trust principles",
"Attributes": [
{
"ItemId": "ksi-iam",
"Section": "Identity and Access Management",
"Service": "azure"
}
],
"Checks": [
"entra_conditional_access_policy_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",
"entra_policy_ensure_default_user_cannot_create_apps",
"entra_policy_ensure_default_user_cannot_create_tenants",
"entra_policy_guest_invite_only_for_admin_roles",
"entra_policy_guest_users_access_restrictions",
"entra_policy_restricts_user_consent_for_apps",
"entra_policy_user_consent_for_verified_apps",
"entra_privileged_user_has_mfa",
"entra_security_defaults_enabled",
"entra_trusted_named_locations_exists",
"entra_user_with_vm_access_has_mfa",
"entra_users_cannot_create_microsoft_365_groups",
"iam_custom_role_has_permissions_to_administer_resource_locks",
"iam_role_user_access_admin_restricted",
"iam_subscription_roles_owner_custom_not_created",
"keyvault_rbac_enabled",
"app_function_identity_is_configured",
"app_function_identity_without_admin_privileges",
"app_ensure_auth_is_set_up",
"app_register_with_identity",
"vm_managed_identity_enabled"
]
},
{
"Id": "ksi-inr",
"Name": "KSI-INR: Incident Response",
"Description": "A secure cloud service offering will respond to incidents according to FedRAMP requirements and cloud service provider policies",
"Attributes": [
{
"ItemId": "ksi-inr",
"Section": "Incident Response",
"Service": "azure"
}
],
"Checks": [
"defender_attack_path_notifications_properly_configured",
"defender_ensure_notify_alerts_severity_is_high",
"defender_ensure_notify_emails_to_owners",
"defender_additional_email_configured_with_a_security_contact",
"defender_container_images_resolved_vulnerabilities",
"defender_container_images_scan_enabled",
"defender_ensure_defender_for_app_services_is_on",
"defender_ensure_defender_for_arm_is_on",
"defender_ensure_defender_for_azure_sql_databases_is_on",
"defender_ensure_defender_for_containers_is_on",
"defender_ensure_defender_for_cosmosdb_is_on",
"defender_ensure_defender_for_databases_is_on",
"defender_ensure_defender_for_dns_is_on",
"defender_ensure_defender_for_keyvault_is_on",
"defender_ensure_defender_for_os_relational_databases_is_on",
"defender_ensure_defender_for_server_is_on",
"defender_ensure_defender_for_sql_servers_is_on",
"defender_ensure_defender_for_storage_is_on",
"defender_ensure_iot_hub_defender_is_on",
"defender_ensure_wdatp_is_enabled"
]
},
{
"Id": "ksi-mla",
"Name": "KSI-MLA: Monitoring, Logging, and Auditing",
"Description": "A secure cloud service offering will monitor, log, and audit all important events, activity, and changes",
"Attributes": [
{
"ItemId": "ksi-mla",
"Section": "Monitoring, Logging, and Auditing",
"Service": "azure"
}
],
"Checks": [
"app_function_application_insights_enabled",
"app_http_logs_enabled",
"appinsights_ensure_is_configured",
"defender_auto_provisioning_log_analytics_agent_vms_on",
"defender_auto_provisioning_vulnerabilty_assessments_machines_on",
"keyvault_logging_enabled",
"monitor_activity_log_retention_policy_set",
"monitor_diagnostic_logs_categories",
"monitor_diagnostic_setting_deployed_for_all_resources",
"monitor_diagnostic_settings_captures_proper_categories",
"monitor_log_profile_all_categories",
"monitor_log_profile_all_regions",
"monitor_log_profile_captures_all_activities",
"monitor_log_profile_retention_policy_at_least_365",
"network_flow_logs_enabled",
"network_flow_log_retention_policy_at_least_90",
"network_watcher_enabled",
"postgresql_flexible_server_audit_logs_enabled",
"postgresql_flexible_server_log_checkpoints_enabled",
"postgresql_flexible_server_log_connections_enabled",
"postgresql_flexible_server_log_disconnections_enabled",
"sqlserver_auditing_on",
"sqlserver_auditing_retention_90_days",
"storage_storage_account_logging_queue_read_write_delete_enabled"
]
},
{
"Id": "ksi-piy",
"Name": "KSI-PIY: Policy and Inventory",
"Description": "A secure cloud service offering will have intentional, organized, universal guidance for how every information resource, including personnel, is secured",
"Attributes": [
{
"ItemId": "ksi-piy",
"Section": "Policy and Inventory",
"Service": "azure"
}
],
"Checks": [
"policy_ensure_asc_for_aks_is_enabled",
"policy_ensure_asc_for_app_services_is_enabled",
"policy_ensure_asc_for_azure_sql_is_enabled",
"policy_ensure_asc_for_key_vault_is_enabled",
"policy_ensure_asc_for_servers_is_enabled",
"policy_ensure_asc_for_sql_servers_is_enabled",
"policy_ensure_asc_for_storage_is_enabled",
"policy_ensure_allowed_extensions_are_installed",
"policy_ensure_allowed_locations_is_enabled",
"policy_ensure_allowed_resource_types_is_enabled",
"policy_ensure_audit_diagnostic_log_enabled_for_all_services",
"policy_ensure_not_allowed_resource_types_is_enabled",
"vm_guest_configuration_installed",
"vm_guest_configuration_with_no_managed_identity",
"vm_guest_configuration_with_user_identity"
]
},
{
"Id": "ksi-rpl",
"Name": "KSI-RPL: Recovery Planning",
"Description": "A secure cloud service offering will define, maintain, and test incident response plan(s) and recovery capabilities to ensure minimal service disruption and data loss",
"Attributes": [
{
"ItemId": "ksi-rpl",
"Section": "Recovery Planning",
"Service": "azure"
}
],
"Checks": [
"mysql_flexible_server_geo_redundant_backup_enabled",
"mysql_flexible_server_retain_backup_35_days",
"postgresql_flexible_server_geo_redundant_backup_enabled",
"postgresql_flexible_server_backup_retention_period_35_days",
"recovery_services_vault_uses_private_link",
"recovery_services_vault_uses_private_link_for_backup",
"sqlserver_database_long_term_geo_redundant_backup",
"sqlserver_database_retention_policy_exceeds_90_days",
"storage_default_storage_account_encrypted_with_cmk_not_stored_in_storage_account",
"storage_geo_redundant_enabled",
"storage_infrastructure_encryption_is_enabled",
"storage_soft_delete_containers_enabled",
"storage_soft_delete_enabled",
"vm_backup_enabled",
"vm_sufficient_daily_backup_retention_period"
]
},
{
"Id": "ksi-svc",
"Name": "KSI-SVC: Service Configuration",
"Description": "A secure cloud service offering will follow FedRAMP encryption policies, continuously verify information resource integrity, and restrict access to third-party information resources",
"Attributes": [
{
"ItemId": "ksi-svc",
"Section": "Service Configuration",
"Service": "azure"
}
],
"Checks": [
"app_client_certificates_on",
"app_ensure_http_is_redirected_to_https",
"app_minimum_tls_version_12",
"containerregistry_admin_user_disabled",
"cosmosdb_account_use_aad_and_rbac",
"databricks_workspace_cmk_encryption_enabled",
"keyvault_key_expiration_set_in_non_rbac",
"keyvault_key_rotation_enabled",
"keyvault_non_rbac_secret_expiration_set",
"mysql_flexible_server_encrypted_at_rest_using_cmk",
"mysql_flexible_server_encrypted_in_transit",
"mysql_flexible_server_minimum_tls_version_tls12",
"postgresql_flexible_server_encrypted_at_rest_using_cmk",
"postgresql_flexible_server_encrypted_in_transit",
"postgresql_flexible_server_minimum_tls_version_tls12",
"sqlserver_advanced_data_security_enabled",
"sqlserver_database_encryption_with_cmk",
"sqlserver_database_tde_encryption_enabled",
"sqlserver_minimum_tls_version_12",
"storage_secure_transfer_required_enabled",
"storage_default_storage_account_encrypted_with_cmk",
"storage_infrastructure_encryption_is_enabled",
"storage_storage_account_encrypted_with_cmk",
"storage_storage_account_minimum_tls_version_tls12",
"vm_encrypted_at_host",
"vm_data_disks_encrypted_with_cmk",
"vm_managed_disks_encrypted_with_cmk",
"vm_os_disk_are_encrypted_with_cmk",
"vm_temporary_disks_and_cache_encrypted"
]
},
{
"Id": "ksi-tpr",
"Name": "KSI-TPR: Third-Party Information Resources",
"Description": "A secure cloud service offering will understand, monitor, and manage supply chain risks from third-party information resources",
"Attributes": [
{
"ItemId": "ksi-tpr",
"Section": "Third-Party Information Resources",
"Service": "azure"
}
],
"Checks": [
"app_ensure_java_version_is_latest",
"app_ensure_php_version_is_latest",
"app_ensure_python_version_is_latest",
"app_function_latest_runtime_version",
"defender_container_images_resolved_vulnerabilities",
"defender_container_images_scan_enabled",
"defender_ensure_system_updates_are_applied",
"vm_agent_installed",
"vm_antimalware_solution_installed",
"vm_endpoint_protection_installed",
"vm_os_update_system_updates",
"vm_security_patch_assessment"
]
},
{
"Id": "ksi-iam-07",
"Name": "KSI-IAM-07: Account Lifecycle Management",
"Description": "Securely manage the lifecycle and privileges of all accounts, roles, and groups",
"Attributes": [
{
"ItemId": "ksi-iam-07",
"Section": "Identity and Access Management",
"Service": "azure"
}
],
"Checks": [
"entra_non_privileged_user_has_mfa",
"entra_privileged_user_has_mfa",
"entra_user_with_vm_access_has_mfa",
"iam_custom_role_has_permissions_to_administer_resource_locks",
"iam_role_user_access_admin_restricted",
"app_function_identity_is_configured",
"vm_managed_identity_enabled"
]
},
{
"Id": "ksi-mla-07",
"Name": "KSI-MLA-07: Monitoring and Logging Inventory",
"Description": "Maintain a list of information resources and event types that will be monitored, logged, and audited",
"Attributes": [
{
"ItemId": "ksi-mla-07",
"Section": "Monitoring, Logging, and Auditing",
"Service": "azure"
}
],
"Checks": [
"monitor_log_profile_all_categories",
"monitor_log_profile_all_regions",
"monitor_log_profile_captures_all_activities",
"monitor_diagnostic_setting_deployed_for_all_resources",
"network_watcher_enabled"
]
}
]
}
@@ -0,0 +1,293 @@
{
"Framework": "FedRAMP-20x-KSI-Low",
"Name": "FedRAMP 20x Key Security Indicators (KSIs) - Low Impact Level v25.05C",
"Version": "25.05C",
"Provider": "GCP",
"Description": "FedRAMP 20x Key Security Indicators (KSIs) Low Impact Level represent core security indicators for cloud service providers, focusing on automation, continuous monitoring, and cloud-native security principles per FedRAMP 20x Phase One pilot requirements for Low impact systems.",
"Requirements": [
{
"Id": "ksi-cmt",
"Name": "KSI-CMT: Change Management",
"Description": "A secure cloud service provider will ensure that all system changes are properly documented and configuration baselines are updated accordingly",
"Attributes": [
{
"ItemId": "ksi-cmt",
"Section": "Change Management",
"Service": "gcp"
}
],
"Checks": [
"iam_audit_logs_enabled",
"iam_cloud_asset_inventory_enabled",
"logging_log_metric_filter_and_alert_for_audit_configuration_changes_enabled",
"logging_log_metric_filter_and_alert_for_bucket_permission_changes_enabled",
"logging_log_metric_filter_and_alert_for_custom_role_changes_enabled",
"logging_log_metric_filter_and_alert_for_project_ownership_changes_enabled",
"logging_log_metric_filter_and_alert_for_sql_instance_configuration_changes_enabled",
"logging_log_metric_filter_and_alert_for_vpc_firewall_rule_changes_enabled",
"logging_log_metric_filter_and_alert_for_vpc_network_changes_enabled",
"logging_log_metric_filter_and_alert_for_vpc_network_route_changes_enabled",
"compute_instance_serial_ports_in_use",
"compute_project_os_login_enabled"
]
},
{
"Id": "ksi-cna",
"Name": "KSI-CNA: Cloud Native Architecture",
"Description": "A secure cloud service offering will use cloud native architecture and design principles to enforce and enhance the Confidentiality, Integrity and Availability of the system",
"Attributes": [
{
"ItemId": "ksi-cna",
"Section": "Cloud Native Architecture",
"Service": "gcp"
}
],
"Checks": [
"cloudsql_instance_private_ip_assignment",
"cloudsql_instance_public_access",
"cloudsql_instance_public_ip",
"cloudstorage_bucket_uniform_bucket_level_access",
"compute_firewall_rdp_access_from_the_internet_allowed",
"compute_firewall_ssh_access_from_the_internet_allowed",
"compute_instance_block_project_wide_ssh_keys_disabled",
"compute_instance_confidential_computing_enabled",
"compute_instance_ip_forwarding_is_enabled",
"compute_instance_public_ip",
"compute_instance_shielded_vm_enabled",
"compute_loadbalancer_logging_enabled",
"compute_network_default_in_use",
"compute_network_dns_logging_enabled",
"compute_network_not_legacy",
"compute_subnet_flow_logs_enabled",
"gke_cluster_no_default_service_account"
]
},
{
"Id": "ksi-iam",
"Name": "KSI-IAM: Identity and Access Management",
"Description": "A secure cloud service offering will protect user data, control access, and apply zero trust principles",
"Attributes": [
{
"ItemId": "ksi-iam",
"Section": "Identity and Access Management",
"Service": "gcp"
}
],
"Checks": [
"apikeys_api_restrictions_configured",
"apikeys_key_exists",
"apikeys_key_rotated_in_90_days",
"compute_instance_default_service_account_in_use",
"compute_instance_default_service_account_in_use_with_full_api_access",
"iam_no_service_roles_at_project_level",
"iam_role_kms_enforce_separation_of_duties",
"iam_role_sa_enforce_separation_of_duties",
"iam_sa_no_administrative_privileges",
"iam_sa_no_user_managed_keys",
"iam_sa_user_managed_key_rotate_90_days",
"iam_sa_user_managed_key_unused",
"iam_service_account_unused"
]
},
{
"Id": "ksi-inr",
"Name": "KSI-INR: Incident Response",
"Description": "A secure cloud service offering will respond to incidents according to FedRAMP requirements and cloud service provider policies",
"Attributes": [
{
"ItemId": "ksi-inr",
"Section": "Incident Response",
"Service": "gcp"
}
],
"Checks": [
"iam_organization_essential_contacts_configured",
"iam_account_access_approval_enabled",
"logging_log_metric_filter_and_alert_for_audit_configuration_changes_enabled",
"logging_log_metric_filter_and_alert_for_bucket_permission_changes_enabled",
"logging_log_metric_filter_and_alert_for_custom_role_changes_enabled",
"logging_log_metric_filter_and_alert_for_project_ownership_changes_enabled",
"logging_log_metric_filter_and_alert_for_sql_instance_configuration_changes_enabled",
"logging_log_metric_filter_and_alert_for_vpc_firewall_rule_changes_enabled",
"logging_log_metric_filter_and_alert_for_vpc_network_changes_enabled",
"logging_log_metric_filter_and_alert_for_vpc_network_route_changes_enabled"
]
},
{
"Id": "ksi-mla",
"Name": "KSI-MLA: Monitoring, Logging, and Auditing",
"Description": "A secure cloud service offering will monitor, log, and audit all important events, activity, and changes",
"Attributes": [
{
"ItemId": "ksi-mla",
"Section": "Monitoring, Logging, and Auditing",
"Service": "gcp"
}
],
"Checks": [
"cloudsql_instance_postgres_enable_pgaudit_flag",
"cloudsql_instance_postgres_log_connections_flag",
"cloudsql_instance_postgres_log_disconnections_flag",
"cloudsql_instance_postgres_log_error_verbosity_flag",
"cloudsql_instance_postgres_log_min_duration_statement_flag",
"cloudsql_instance_postgres_log_min_error_statement_flag",
"cloudsql_instance_postgres_log_min_messages_flag",
"cloudsql_instance_postgres_log_statement_flag",
"cloudsql_instance_sqlserver_trace_flag",
"cloudstorage_bucket_log_retention_policy_lock",
"compute_loadbalancer_logging_enabled",
"compute_network_dns_logging_enabled",
"compute_subnet_flow_logs_enabled",
"iam_audit_logs_enabled",
"logging_log_metric_filter_and_alert_for_audit_configuration_changes_enabled",
"logging_log_metric_filter_and_alert_for_bucket_permission_changes_enabled",
"logging_log_metric_filter_and_alert_for_custom_role_changes_enabled",
"logging_log_metric_filter_and_alert_for_project_ownership_changes_enabled",
"logging_log_metric_filter_and_alert_for_sql_instance_configuration_changes_enabled",
"logging_log_metric_filter_and_alert_for_vpc_firewall_rule_changes_enabled",
"logging_log_metric_filter_and_alert_for_vpc_network_changes_enabled",
"logging_log_metric_filter_and_alert_for_vpc_network_route_changes_enabled",
"logging_sink_created"
]
},
{
"Id": "ksi-piy",
"Name": "KSI-PIY: Policy and Inventory",
"Description": "A secure cloud service offering will have intentional, organized, universal guidance for how every information resource, including personnel, is secured",
"Attributes": [
{
"ItemId": "ksi-piy",
"Section": "Policy and Inventory",
"Service": "gcp"
}
],
"Checks": [
"iam_cloud_asset_inventory_enabled",
"iam_organization_essential_contacts_configured",
"iam_audit_logs_enabled",
"compute_project_os_login_enabled",
"compute_instance_serial_ports_in_use",
"compute_instance_block_project_wide_ssh_keys_disabled",
"logging_sink_created"
]
},
{
"Id": "ksi-rpl",
"Name": "KSI-RPL: Recovery Planning",
"Description": "A secure cloud service offering will define, maintain, and test incident response plan(s) and recovery capabilities to ensure minimal service disruption and data loss",
"Attributes": [
{
"ItemId": "ksi-rpl",
"Section": "Recovery Planning",
"Service": "gcp"
}
],
"Checks": [
"cloudsql_instance_automated_backups",
"cloudstorage_bucket_log_retention_policy_lock",
"cloudstorage_bucket_versioning_enabled",
"cloudstorage_bucket_lifecycle_management_enabled"
]
},
{
"Id": "ksi-svc",
"Name": "KSI-SVC: Service Configuration",
"Description": "A secure cloud service offering will follow FedRAMP encryption policies, continuously verify information resource integrity, and restrict access to third-party information resources",
"Attributes": [
{
"ItemId": "ksi-svc",
"Section": "Service Configuration",
"Service": "gcp"
}
],
"Checks": [
"bigquery_dataset_cmk_encryption",
"bigquery_table_cmk_encryption",
"cloudsql_instance_mysql_local_infile_flag",
"cloudsql_instance_mysql_skip_show_database_flag",
"cloudsql_instance_postgres_enable_pgaudit_flag",
"cloudsql_instance_postgres_log_connections_flag",
"cloudsql_instance_postgres_log_disconnections_flag",
"cloudsql_instance_postgres_log_error_verbosity_flag",
"cloudsql_instance_postgres_log_min_duration_statement_flag",
"cloudsql_instance_postgres_log_min_error_statement_flag",
"cloudsql_instance_postgres_log_min_messages_flag",
"cloudsql_instance_postgres_log_statement_flag",
"cloudsql_instance_sqlserver_contained_database_authentication_flag",
"cloudsql_instance_sqlserver_cross_db_ownership_chaining_flag",
"cloudsql_instance_sqlserver_external_scripts_enabled_flag",
"cloudsql_instance_sqlserver_remote_access_flag",
"cloudsql_instance_sqlserver_trace_flag",
"cloudsql_instance_sqlserver_user_connections_flag",
"cloudsql_instance_sqlserver_user_options_flag",
"cloudsql_instance_ssl_connections",
"compute_instance_encryption_with_csek_enabled",
"compute_instance_shielded_vm_enabled",
"dataproc_encrypted_with_cmks_disabled",
"dns_dnssec_disabled",
"dns_rsasha1_in_use_to_key_sign_in_dnssec",
"dns_rsasha1_in_use_to_zone_sign_in_dnssec",
"kms_key_not_publicly_accessible",
"kms_key_rotation_enabled"
]
},
{
"Id": "ksi-tpr",
"Name": "KSI-TPR: Third-Party Information Resources",
"Description": "A secure cloud service offering will understand, monitor, and manage supply chain risks from third-party information resources",
"Attributes": [
{
"ItemId": "ksi-tpr",
"Section": "Third-Party Information Resources",
"Service": "gcp"
}
],
"Checks": [
"artifacts_container_analysis_enabled",
"gcr_container_scanning_enabled",
"compute_public_address_shodan",
"cloudsql_instance_automated_backups",
"iam_sa_user_managed_key_rotate_90_days",
"iam_service_account_unused"
]
},
{
"Id": "ksi-iam-07",
"Name": "KSI-IAM-07: Account Lifecycle Management",
"Description": "Securely manage the lifecycle and privileges of all accounts, roles, and groups",
"Attributes": [
{
"ItemId": "ksi-iam-07",
"Section": "Identity and Access Management",
"Service": "gcp"
}
],
"Checks": [
"apikeys_key_rotated_in_90_days",
"iam_sa_user_managed_key_rotate_90_days",
"iam_sa_user_managed_key_unused",
"iam_service_account_unused",
"compute_instance_default_service_account_in_use"
]
},
{
"Id": "ksi-mla-07",
"Name": "KSI-MLA-07: Monitoring and Logging Inventory",
"Description": "Maintain a list of information resources and event types that will be monitored, logged, and audited",
"Attributes": [
{
"ItemId": "ksi-mla-07",
"Section": "Monitoring, Logging, and Auditing",
"Service": "gcp"
}
],
"Checks": [
"iam_audit_logs_enabled",
"iam_cloud_asset_inventory_enabled",
"logging_sink_created",
"compute_subnet_flow_logs_enabled",
"compute_network_dns_logging_enabled"
]
}
]
}
+3
View File
@@ -511,6 +511,9 @@ gcp:
# gcp.iam_service_account_unused
# gcp.iam_sa_user_managed_key_unused
max_unused_account_days: 180
# GCP Storage Sufficient Retention Period
# gcp.cloudstorage_bucket_sufficient_retention_period
storage_min_retention_days: 90
# Kubernetes Configuration
kubernetes:
+2 -1
View File
@@ -457,7 +457,8 @@ class Check(ABC, CheckMetadata):
# Verify names consistency
check_id = self.CheckID
class_name = self.__class__.__name__
file_name = file_path.split(sep="/")[-1]
# os.path.basename handles Windows and POSIX paths reliably
file_name = os.path.basename(file_path)
errors = []
if check_id != class_name:
+11 -4
View File
@@ -15,6 +15,7 @@ from prowler.lib.check.models import Severity
from prowler.lib.outputs.common import Status
from prowler.providers.common.arguments import (
init_providers_parser,
validate_asff_usage,
validate_provider_arguments,
)
@@ -135,6 +136,12 @@ Detailed documentation at https://docs.prowler.com
if not valid:
self.parser.error(f"{args.provider}: {message}")
asff_is_valid, asff_error = validate_asff_usage(
args.provider, getattr(args, "output_formats", None)
)
if not asff_is_valid:
self.parser.error(asff_error)
return args
def __set_default_provider__(self, args: list) -> list:
@@ -304,7 +311,7 @@ Detailed documentation at https://docs.prowler.com
"--checks-folder",
"-x",
nargs="?",
help="Specify external directory with custom checks (each check must have a folder with the required files, see more in https://docs.prowler.cloud/en/latest/tutorials/misc/#custom-checks).",
help="Specify external directory with custom checks (each check must have a folder with the required files, see more in https://docs.prowler.com/user-guide/cli/tutorials/misc#custom-checks-in-prowler).",
)
def __init_list_checks_parser__(self):
@@ -357,7 +364,7 @@ Detailed documentation at https://docs.prowler.com
"--mutelist-file",
"-w",
nargs="?",
help="Path for mutelist YAML file. See example prowler/config/<provider>_mutelist.yaml for reference and format. For AWS provider, it also accepts AWS DynamoDB Table, Lambda ARNs or S3 URIs, see more in https://docs.prowler.cloud/en/latest/tutorials/mutelist/",
help="Path for mutelist YAML file. See example prowler/config/<provider>_mutelist.yaml for reference and format. For AWS provider, it also accepts AWS DynamoDB Table, Lambda ARNs or S3 URIs, see more in https://docs.prowler.com/user-guide/cli/tutorials/mutelist",
)
def __init_config_parser__(self):
@@ -384,7 +391,7 @@ Detailed documentation at https://docs.prowler.com
"--custom-checks-metadata-file",
nargs="?",
default=None,
help="Path for the custom checks metadata YAML file. See example prowler/config/custom_checks_metadata_example.yaml for reference and format. See more in https://docs.prowler.cloud/en/latest/tutorials/custom-checks-metadata/",
help="Path for the custom checks metadata YAML file. See example prowler/config/custom_checks_metadata_example.yaml for reference and format. See more in https://docs.prowler.com/user-guide/cli/tutorials/custom-checks-metadata/",
)
def __init_third_party_integrations_parser__(self):
@@ -402,5 +409,5 @@ Detailed documentation at https://docs.prowler.com
third_party_subparser.add_argument(
"--slack",
action="store_true",
help="Send a summary of the execution with a Slack APP in your channel. Environment variables SLACK_API_TOKEN and SLACK_CHANNEL_NAME are required (see more in https://docs.prowler.cloud/en/latest/tutorials/integrations/#slack).",
help="Send a summary of the execution with a Slack APP in your channel. Environment variables SLACK_API_TOKEN and SLACK_CHANNEL_NAME are required (see more in https://docs.prowler.com/user-guide/cli/tutorials/integrations#configuration-of-the-integration-with-slack/).",
)
+15 -7
View File
@@ -153,8 +153,10 @@ class Mutelist(ABC):
Check if the provided finding is muted for the audited account, check, region, resource and tags.
The Mutelist works in a way that each field is ANDed, so if a check is muted for an account, region, resource and tags, it will be muted.
The exceptions are ORed, so if a check is excepted for an account, region, resource or tags, it will not be muted.
The only particularity is the tags, which are ORed.
Exceptions use AND logic across specified fields, with unspecified fields treated as wildcards (matching all values).
Tag matching uses AND logic when multiple tags are listed (all must match). OR logic is achieved using regex alternation (|) within a single tag pattern.
So, for the following Mutelist:
```
@@ -167,11 +169,16 @@ class Mutelist(ABC):
Resources:
- 'i-123456789'
Tags:
- 'Name=AdminInstance | Environment=Prod'
- 'Name=AdminInstance|Environment=Prod'
Description: 'Field to describe why the findings associated with these values are muted'
```
The check `ec2_instance_detailed_monitoring_enabled` will be muted for all accounts and regions and for the resource_id 'i-123456789' with at least one of the tags 'Name=AdminInstance' or 'Environment=Prod'.
Note: The pipe (|) in the tag pattern provides OR logic via regex alternation. To require BOTH tags, use two separate tag entries:
Tags:
- 'Name=AdminInstance'
- 'Environment=Prod'
Args:
mutelist (dict): Dictionary containing information about muted checks for different accounts.
audited_account (str): The account being audited.
@@ -408,12 +415,13 @@ class Mutelist(ABC):
Args:
matched_items (list): List of items to be matched.
finding_items (str): String to search for matched items.
tag (bool): If True the search will have a different logic due to the tags being ANDed or ORed:
- Check of AND logic -> True if all the tags are present in the finding.
- Check of OR logic -> True if any of the tags is present in the finding.
tag (bool): If True, uses AND logic across multiple tags in the list.
- Multiple tags: ALL tags in matched_items must be present in finding_items (AND logic).
- Single tag with regex alternation (|): Matches if pattern is found (enables OR within pattern).
- For non-tags: Uses OR logic - returns True if ANY item matches.
Returns:
bool: True if any of the matched_items are present in finding_items, otherwise False.
bool: For tags - True if ALL patterns match. For non-tags - True if ANY pattern matches.
"""
try:
is_item_matched = False
+9 -7
View File
@@ -974,18 +974,20 @@ class HTML(Output):
return ""
@staticmethod
def get_oci_assessment_summary(provider: Provider) -> str:
def get_oraclecloud_assessment_summary(provider: Provider) -> str:
"""
get_oci_assessment_summary gets the HTML assessment summary for the OCI provider
get_oraclecloud_assessment_summary gets the HTML assessment summary for the OracleCloud provider
Args:
provider (Provider): the OCI provider object
provider (Provider): the OracleCloud provider object
Returns:
str: HTML assessment summary for the OCI provider
str: HTML assessment summary for the OracleCloud provider
"""
try:
profile = getattr(provider.session, "profile", "default")
if profile is None:
profile = "instance-principal"
tenancy_name = getattr(provider.identity, "tenancy_name", "unknown")
tenancy_id = getattr(provider.identity, "tenancy_id", "unknown")
@@ -993,11 +995,11 @@ class HTML(Output):
<div class="col-md-2">
<div class="card">
<div class="card-header">
OCI Assessment Summary
OracleCloud Assessment Summary
</div>
<ul class="list-group list-group-flush">
<li class="list-group-item">
<b>OCI Tenancy:</b> {tenancy_name if tenancy_name != "unknown" else tenancy_id}
<b>OracleCloud Tenancy:</b> {tenancy_name if tenancy_name != "unknown" else tenancy_id}
</li>
</ul>
</div>
@@ -1005,7 +1007,7 @@ class HTML(Output):
<div class="col-md-4">
<div class="card">
<div class="card-header">
OCI Credentials
OracleCloud Credentials
</div>
<ul class="list-group list-group-flush">
<li class="list-group-item">
+12 -11
View File
@@ -220,18 +220,19 @@ class PowerShellSession:
if output == "":
return {}
json_match = re.search(r"(\[.*\]|\{.*\})", output, re.DOTALL)
if not json_match:
logger.error(
f"Unexpected PowerShell output: {output}\n",
)
else:
decoder = json.JSONDecoder()
for index, character in enumerate(output):
if character not in ("{", "["):
continue
try:
return json.loads(json_match.group(1))
except json.JSONDecodeError as error:
logger.error(
f"Error parsing PowerShell output as JSON: {str(error)}\n",
)
parsed_json, _ = decoder.raw_decode(output[index:])
return parsed_json
except json.JSONDecodeError:
continue
logger.error(
f"Unexpected PowerShell output: {output}\n",
)
return {}
+1 -1
View File
@@ -64,7 +64,7 @@ def open_file(input_file: str, mode: str = "r") -> TextIOWrapper:
except OSError as os_error:
if os_error.strerror == "Too many open files":
logger.critical(
"Ooops! You reached your user session maximum open files. To solve this issue, increase the shell session limit by running this command `ulimit -n 4096`. For more info visit https://docs.prowler.cloud/en/latest/troubleshooting/"
"Ooops! You reached your user session maximum open files. To solve this issue, increase the shell session limit by running this command `ulimit -n 4096`. For more info visit https://docs.prowler.com/troubleshooting/"
)
else:
logger.critical(
@@ -1454,6 +1454,23 @@
]
}
},
"bedrock-agentcore": {
"regions": {
"aws": [
"ap-northeast-1",
"ap-south-1",
"ap-southeast-1",
"ap-southeast-2",
"eu-central-1",
"eu-west-1",
"us-east-1",
"us-east-2",
"us-west-2"
],
"aws-cn": [],
"aws-us-gov": []
}
},
"bedrock-data-automation": {
"regions": {
"aws": [
@@ -1555,6 +1572,7 @@
"aws": [
"af-south-1",
"ap-east-1",
"ap-east-2",
"ap-northeast-1",
"ap-northeast-2",
"ap-northeast-3",
@@ -1565,6 +1583,8 @@
"ap-southeast-3",
"ap-southeast-4",
"ap-southeast-5",
"ap-southeast-6",
"ap-southeast-7",
"ca-central-1",
"ca-west-1",
"eu-central-1",
@@ -1578,6 +1598,7 @@
"il-central-1",
"me-central-1",
"me-south-1",
"mx-central-1",
"sa-east-1",
"us-east-1",
"us-east-2",
@@ -4584,8 +4605,10 @@
"ap-southeast-2",
"ca-central-1",
"eu-central-1",
"eu-south-1",
"eu-west-1",
"eu-west-2",
"eu-west-3",
"us-east-1",
"us-east-2",
"us-west-2"
@@ -6967,21 +6990,6 @@
"aws-us-gov": []
}
},
"lookoutvision": {
"regions": {
"aws": [
"ap-northeast-1",
"ap-northeast-2",
"eu-central-1",
"eu-west-1",
"us-east-1",
"us-east-2",
"us-west-2"
],
"aws-cn": [],
"aws-us-gov": []
}
},
"lumberyard": {
"regions": {
"aws": [
@@ -7261,6 +7269,7 @@
"eu-west-1",
"eu-west-2",
"eu-west-3",
"me-central-1",
"me-south-1",
"sa-east-1",
"us-east-1",
@@ -7892,6 +7901,7 @@
"ap-southeast-3",
"ap-southeast-4",
"ap-southeast-5",
"ap-southeast-6",
"ap-southeast-7",
"ca-central-1",
"ca-west-1",
@@ -7953,6 +7963,7 @@
"aws": [
"af-south-1",
"ap-east-1",
"ap-east-2",
"ap-northeast-1",
"ap-northeast-2",
"ap-northeast-3",
@@ -7963,6 +7974,8 @@
"ap-southeast-3",
"ap-southeast-4",
"ap-southeast-5",
"ap-southeast-6",
"ap-southeast-7",
"ca-central-1",
"ca-west-1",
"eu-central-1",
@@ -7976,6 +7989,7 @@
"il-central-1",
"me-central-1",
"me-south-1",
"mx-central-1",
"sa-east-1",
"us-east-1",
"us-east-2",
@@ -9799,6 +9813,20 @@
]
}
},
"rtbfabric": {
"regions": {
"aws": [
"ap-northeast-1",
"ap-southeast-1",
"eu-central-1",
"eu-west-1",
"us-east-1",
"us-west-2"
],
"aws-cn": [],
"aws-us-gov": []
}
},
"rum": {
"regions": {
"aws": [
@@ -297,7 +297,7 @@ def create_output(resources: list, provider: AwsProvider, args):
csv_file.close()
print(
f"\n{Fore.YELLOW}WARNING: Only resources that have or have had tags will appear (except for IAM and S3).\nSee more in https://docs.prowler.cloud/en/latest/tutorials/quick-inventory/#objections{Style.RESET_ALL}"
f"\n{Fore.YELLOW}WARNING: Only resources that have or have had tags will appear (except for IAM and S3).\nSee more in https://docs.prowler.com/user-guide/cli/tutorials/quick-inventory/#objections{Style.RESET_ALL}"
)
print("\nMore details in files:")
print(f" - CSV: {args.output_directory}/{output_file + csv_file_suffix}")
@@ -256,7 +256,7 @@ class SecurityHub:
security_hub_client.list_enabled_products_for_import()
):
logger.warning(
f"Security Hub is enabled in {region} but Prowler integration does not accept findings. More info: https://docs.prowler.cloud/en/latest/tutorials/aws/securityhub/"
f"Security Hub is enabled in {region} but Prowler integration does not accept findings. More info: https://docs.prowler.com/user-guide/providers/aws/securityhub#aws-security-hub-integration-with-prowler"
)
return region, None
else:
@@ -32,9 +32,7 @@
"Url": "https://hub.prowler.com/check/awslambda_function_using_supported_runtimes"
}
},
"Categories": [
"container-security"
],
"Categories": [],
"DependsOn": [],
"RelatedTo": [],
"Notes": ""
@@ -1,29 +1,40 @@
{
"Provider": "aws",
"CheckID": "documentdb_cluster_backup_enabled",
"CheckTitle": "Check if DocumentDB Clusters have backup enabled.",
"CheckType": [],
"CheckTitle": "DocumentDB cluster has automated backups enabled with retention period of at least 7 days",
"CheckType": [
"Software and Configuration Checks/AWS Security Best Practices",
"Industry and Regulatory Standards/AWS Foundational Security Best Practices",
"Effects/Data Destruction"
],
"ServiceName": "documentdb",
"SubServiceName": "",
"ResourceIdTemplate": "arn:aws:rds:region:account-id:db-cluster",
"ResourceIdTemplate": "",
"Severity": "medium",
"ResourceType": "AwsRdsDbCluster",
"Description": "Check if DocumentDB Clusters have backup enabled.",
"Risk": "Ensure that your Amazon DocumentDB database clusters have set a minimum backup retention period in order to achieve compliance requirements in your organization.",
"RelatedUrl": "https://docs.aws.amazon.com/securityhub/latest/userguide/documentdb-controls.html#documentdb-2",
"Description": "**Amazon DocumentDB clusters** are evaluated for **automated backups** and an adequate **backup retention period**. Clusters should have `backup_retention_period` set to at least the configured minimum (default `7` days). Values of `0` indicate backups are disabled; values below the threshold are considered insufficient.",
"Risk": "Without adequate backups, clusters can't be reliably restored. Accidental deletes, logical corruption, or ransomware may cause irreversible data loss once a short retention window expires, leading to prolonged outages, missed RPO/RTO, and limited ability to roll back malicious or erroneous changes.",
"RelatedUrl": "",
"AdditionalURLs": [
"https://docs.amazonaws.cn/en_us/documentdb/latest/developerguide/what-is.html",
"https://www.trendmicro.com/cloudoneconformity-staging/knowledge-base/aws/DocumentDB/sufficient-backup-retention-period.html#",
"https://docs.aws.amazon.com/systems-manager-automation-runbooks/latest/userguide/aws-enabledocdbclusterbackupretentionperiod.html"
],
"Remediation": {
"Code": {
"CLI": "aws docdb modify-db-cluster --region <REGION> --db-cluster-identifier <DB_CLUSTER_ID> --backup-retention-period 7 --apply-immediately",
"NativeIaC": "https://www.trendmicro.com/cloudoneconformity-staging/knowledge-base/aws/DocumentDB/sufficient-backup-retention-period.html#",
"Other": "https://www.trendmicro.com/cloudoneconformity-staging/knowledge-base/aws/DocumentDB/sufficient-backup-retention-period.html#",
"Terraform": "https://www.trendmicro.com/cloudoneconformity-staging/knowledge-base/aws/DocumentDB/sufficient-backup-retention-period.html#"
"CLI": "aws docdb modify-db-cluster --db-cluster-identifier <DB_CLUSTER_ID> --backup-retention-period 7 --apply-immediately",
"NativeIaC": "```yaml\n# CloudFormation: Set DocumentDB backup retention to at least 7 days\nResources:\n <example_resource_name>:\n Type: AWS::DocDB::DBCluster\n Properties:\n BackupRetentionPeriod: 7 # CRITICAL: enables automated backups and sets retention to >=7 days\n```",
"Other": "1. Open the Amazon DocumentDB console\n2. Go to Clusters and select <example_resource_id>\n3. Click Modify\n4. Set Backup retention period to 7 (or higher)\n5. Check Apply immediately\n6. Click Continue and then Modify cluster",
"Terraform": "```hcl\n# Terraform: Ensure DocumentDB backup retention is at least 7 days\nresource \"aws_docdb_cluster\" \"<example_resource_name>\" {\n cluster_identifier = \"<example_resource_id>\"\n backup_retention_period = 7 # CRITICAL: enables automated backups and sets retention to >=7 days\n}\n```"
},
"Recommendation": {
"Text": "Enable automated backup for production data. Define a retention period and periodically test backup restoration. A Disaster Recovery process should be in place to govern Data Protection approach.",
"Url": "https://docs.aws.amazon.com/securityhub/latest/userguide/documentdb-controls.html#documentdb-2"
"Text": "Enable **automated backups** and set retention to meet RPO/RTO (typically `7-35` days).\n- Regularly test point-in-time restores\n- Apply **least privilege** to backup/snapshot management\n- Protect backup artifacts and define stable backup windows\n- Include restores in a tested **disaster recovery** plan",
"Url": "https://hub.prowler.com/check/documentdb_cluster_backup_enabled"
}
},
"Categories": [],
"Categories": [
"resilience"
],
"DependsOn": [],
"RelatedTo": [],
"Notes": ""
@@ -1,29 +1,39 @@
{
"Provider": "aws",
"CheckID": "documentdb_cluster_cloudwatch_log_export",
"CheckTitle": "Check if DocumentDB clusters are using the log export feature.",
"CheckType": [],
"CheckTitle": "DocumentDB cluster exports audit and profiler logs to CloudWatch Logs",
"CheckType": [
"Software and Configuration Checks/AWS Security Best Practices/Runtime Behavior Analysis",
"Software and Configuration Checks/Industry and Regulatory Standards/AWS Foundational Security Best Practices"
],
"ServiceName": "documentdb",
"SubServiceName": "",
"ResourceIdTemplate": "arn:partition:service:region:account-id:resource-id",
"ResourceIdTemplate": "",
"Severity": "medium",
"ResourceType": "AwsRdsDbCluster",
"Description": "Check if DocumentDB clusters are using the log export feature.",
"Risk": "Ensure that all your Amazon DocumentDB clusters are using the Log Exports feature in order to publish audit logs directly to CloudWatch Logs. The events recorded by Log Exports include events such as successful and failed authentication attempts, creating indexes, or dropping collections in DocumentDB databases.",
"RelatedUrl": "https://docs.aws.amazon.com/securityhub/latest/userguide/documentdb-controls.html#documentdb-4",
"Description": "Amazon DocumentDB clusters are evaluated for exporting `audit` and `profiler` logs to **CloudWatch Logs**.\nClusters missing one or both log types are identified as lacking complete log export configuration.",
"Risk": "Missing **audit** and/or **profiler** exports reduces observability of authentication, authorization, and data definition activity.\nAttacks like brute-force logins, privilege abuse, or destructive schema changes can go unnoticed, degrading **confidentiality** and **integrity** and delaying incident response.",
"RelatedUrl": "",
"AdditionalURLs": [
"https://docs.aws.amazon.com/securityhub/latest/userguide/documentdb-controls.html#documentdb-4",
"https://www.trendmicro.com/cloudoneconformity/knowledge-base/aws/DocumentDB/enable-profiler.html",
"https://docs.aws.amazon.com/cli/latest/reference/docdb/create-db-cluster.html"
],
"Remediation": {
"Code": {
"CLI": "aws docdb modify-db-cluster --region <REGION> --db-cluster-identifier <DB_CLUSTER_ID> --db-cluster-parameter-group-name <DB_CLUSTER_PARAMETER_GROUP_NAME> --cloudwatch-logs-export-configuration '{EnableLogTypes:[profiler]}' --apply-immediately",
"NativeIaC": "https://www.trendmicro.com/cloudoneconformity-staging/knowledge-base/aws/DocumentDB/enable-profiler.html",
"Other": "https://www.trendmicro.com/cloudoneconformity-staging/knowledge-base/aws/DocumentDB/enable-profiler.html",
"Terraform": "https://www.trendmicro.com/cloudoneconformity-staging/knowledge-base/aws/DocumentDB/enable-profiler.html"
"CLI": "aws docdb modify-db-cluster --db-cluster-identifier <DB_CLUSTER_ID> --cloudwatch-logs-export-configuration '{\"EnableLogTypes\":[\"audit\",\"profiler\"]}' --apply-immediately",
"NativeIaC": "```yaml\n# CloudFormation: enable DocumentDB log exports\nResources:\n <example_resource_name>:\n Type: AWS::DocDB::DBCluster\n Properties:\n EnableCloudwatchLogsExports:\n - audit # Critical: export audit logs to CloudWatch Logs\n - profiler # Critical: export profiler logs to CloudWatch Logs\n```",
"Other": "1. In AWS Console, go to Amazon DocumentDB > Clusters\n2. Select the cluster and choose Actions > Modify\n3. In Log exports, check Audit and Profiler\n4. Check Apply immediately and click Modify cluster",
"Terraform": "```hcl\n# Enable DocumentDB log exports\nresource \"aws_docdb_cluster\" \"<example_resource_name>\" {\n enabled_cloudwatch_logs_exports = [\"audit\", \"profiler\"] # Critical: export both logs to CloudWatch Logs\n}\n```"
},
"Recommendation": {
"Text": "Enabled DocumentDB Log export functionality to analyze, monitor, and archive auditing events for security and compliance requirements.",
"Url": "https://docs.aws.amazon.com/securityhub/latest/userguide/documentdb-controls.html#documentdb-4"
"Text": "Enable export of both `audit` and `profiler` logs to **CloudWatch Logs** for all clusters and centralize analysis.\nApply **least privilege** to log access, define retention and immutability, integrate with alerting, and use **separation of duties** to protect and regularly review logs for **defense in depth**.",
"Url": "https://hub.prowler.com/check/documentdb_cluster_cloudwatch_log_export"
}
},
"Categories": [],
"Categories": [
"logging"
],
"DependsOn": [],
"RelatedTo": [],
"Notes": ""
@@ -1,29 +1,40 @@
{
"Provider": "aws",
"CheckID": "documentdb_cluster_deletion_protection",
"CheckTitle": "Check if DocumentDB Clusters has deletion protection enabled.",
"CheckType": [],
"CheckTitle": "DocumentDB cluster has deletion protection enabled",
"CheckType": [
"Software and Configuration Checks/AWS Security Best Practices",
"Software and Configuration Checks/Industry and Regulatory Standards/AWS Foundational Security Best Practices"
],
"ServiceName": "documentdb",
"SubServiceName": "",
"ResourceIdTemplate": "arn:aws:rds:region:account-id:db-cluster",
"ResourceIdTemplate": "",
"Severity": "medium",
"ResourceType": "AwsRdsDbCluster",
"Description": "Check if DocumentDB Clusters has deletion protection enabled.",
"Risk": "Enabling cluster deletion protection offers an additional layer of protection against accidental database deletion or deletion by an unauthorized user. A DocumentDB cluster can't be deleted while deletion protection is enabled. You must first disable deletion protection before a delete request can succeed.",
"RelatedUrl": "https://docs.aws.amazon.com/securityhub/latest/userguide/documentdb-controls.html#documentdb-5",
"Description": "**Amazon DocumentDB clusters** are evaluated for the `deletion_protection` setting on the cluster configuration.\n\nThe finding highlights clusters where this protection is not enabled.",
"Risk": "Without **deletion protection**, clusters can be deleted by mistake or misuse, causing sudden outage and loss of recovery points, impacting **availability** and **data integrity**.\n\nCompromised accounts or faulty automation can remove databases or skip final snapshots, hindering restoration.",
"RelatedUrl": "",
"AdditionalURLs": [
"https://support.icompaas.com/support/solutions/articles/62000233689-ensure-documentdb-clusters-has-deletion-protection-enabled",
"https://www.trendmicro.com/cloudoneconformity/knowledge-base/aws/DocumentDB/deletion-protection.html",
"https://docs.aws.amazon.com/documentdb/latest/developerguide/db-cluster-delete.html",
"https://docs.aws.amazon.com/securityhub/latest/userguide/documentdb-controls.html#documentdb-5"
],
"Remediation": {
"Code": {
"CLI": "aws aws docdb modify-db-cluster --region <REGION> --db-cluster-identifier <DB_CLUSTER_ID> --deletion-protection --apply-immediately",
"NativeIaC": "https://www.trendmicro.com/cloudoneconformity-staging/knowledge-base/aws/DocumentDB/deletion-protection.html#",
"Other": "https://www.trendmicro.com/cloudoneconformity-staging/knowledge-base/aws/DocumentDB/deletion-protection.html#",
"Terraform": "https://www.trendmicro.com/cloudoneconformity-staging/knowledge-base/aws/DocumentDB/deletion-protection.html#"
"CLI": "aws docdb modify-db-cluster --db-cluster-identifier <DB_CLUSTER_ID> --deletion-protection --apply-immediately",
"NativeIaC": "```yaml\n# CloudFormation: Enable deletion protection on a DocumentDB cluster\nResources:\n <example_resource_name>:\n Type: AWS::DocDB::DBCluster\n Properties:\n MasterUsername: \"<MASTER_USERNAME>\"\n MasterUserPassword: \"<MASTER_USER_PASSWORD>\"\n DeletionProtection: true # CRITICAL: Prevents cluster deletion until disabled\n```",
"Other": "1. In the AWS Console, go to Amazon DocumentDB > Clusters\n2. Select the target cluster and click Modify\n3. Enable Deletion protection\n4. Check Apply immediately and click Save changes",
"Terraform": "```hcl\n# Terraform: Enable deletion protection on a DocumentDB cluster\nresource \"aws_docdb_cluster\" \"<example_resource_name>\" {\n master_username = \"<MASTER_USERNAME>\"\n master_password = \"<MASTER_USER_PASSWORD>\"\n deletion_protection = true # CRITICAL: Prevents cluster deletion until disabled\n}\n```"
},
"Recommendation": {
"Text": "Enable deletion protection for production DocumentDB Clusters.",
"Url": "https://docs.aws.amazon.com/securityhub/latest/userguide/documentdb-controls.html#documentdb-5"
"Text": "Enable **deletion protection** on all non-ephemeral clusters, prioritizing production.\n\nEnforce **least privilege** for delete and modify actions, require change control to toggle protection, and implement **defense in depth** with automation that continuously enforces this setting. *Before decommissioning*, take a final snapshot.",
"Url": "https://hub.prowler.com/check/documentdb_cluster_deletion_protection"
}
},
"Categories": [],
"Categories": [
"resilience"
],
"DependsOn": [],
"RelatedTo": [],
"Notes": ""
@@ -1,30 +1,36 @@
{
"Provider": "aws",
"CheckID": "documentdb_cluster_multi_az_enabled",
"CheckTitle": "Ensure DocumentDB Cluster have Multi-AZ enabled.",
"CheckType": [],
"CheckTitle": "DocumentDB cluster has Multi-AZ enabled",
"CheckType": [
"Software and Configuration Checks/AWS Security Best Practices"
],
"ServiceName": "documentdb",
"SubServiceName": "",
"ResourceIdTemplate": "arn:aws:rds:region:account-id:db-cluster",
"ResourceIdTemplate": "",
"Severity": "medium",
"ResourceType": "AwsRdsDbCluster",
"Description": "Ensure DocumentDB Cluster have Multi-AZ enabled.",
"Risk": "Ensure that your Amazon DocumentDB Clusters are using Multi-AZ deployment configurations to provide High Availability (HA) through automatic failover to standby replicas in the event of a failure such as an Availability Zone (AZ) outage, an internal hardware or network outage, a software failure or in case of a planned maintenance session.",
"RelatedUrl": "https://docs.aws.amazon.com/documentdb/latest/developerguide/failover.html",
"Description": "**Amazon DocumentDB clusters** with **Multi-AZ** (`multi_az`) indicate deployment of a primary and one or more replicas across Availability Zones.",
"Risk": "Without Multi-AZ, the cluster depends on a single AZ/instance. An AZ or node failure-or maintenance-can stop reads and writes, causing downtime, timeouts, and SLA breaches. Availability degrades, RTO rises, and applications may experience failed or retried transactions until replacement capacity is created.",
"RelatedUrl": "",
"AdditionalURLs": [
"https://docs.aws.amazon.com/documentdb/latest/developerguide/failover.html",
"https://support.icompaas.com/support/solutions/articles/62000233690-ensure-documentdb-cluster-have-multi-az-enabled"
],
"Remediation": {
"Code": {
"CLI": "",
"NativeIaC": "",
"Other": "",
"Terraform": ""
"CLI": "aws docdb create-db-instance --db-instance-identifier <example_resource_id> --db-cluster-identifier <example_resource_id> --db-instance-class <INSTANCE_CLASS> --engine docdb --availability-zone <OTHER_AZ>",
"NativeIaC": "```yaml\n# CloudFormation: add a replica to enable Multi-AZ for an existing DocumentDB cluster\nResources:\n DocDBReplica:\n Type: AWS::DocDB::DBInstance\n Properties:\n DBClusterIdentifier: \"<example_resource_id>\" # CRITICAL: adds a new instance to the cluster to achieve Multi-AZ\n DBInstanceClass: \"<INSTANCE_CLASS>\"\n AvailabilityZone: \"<OTHER_AZ>\" # CRITICAL: place in a different AZ to provide Multi-AZ failover\n```",
"Other": "1. In the AWS Console, go to Amazon DocumentDB and open your cluster\n2. Click Create instance\n3. Set Instance class and choose an Availability Zone different from the primary\n4. Click Create to add the replica\n5. Verify the cluster now shows Multi-AZ enabled",
"Terraform": "```hcl\n# Add a replica to enable Multi-AZ for an existing DocumentDB cluster\nresource \"aws_docdb_cluster_instance\" \"<example_resource_name>\" {\n cluster_identifier = \"<example_resource_id>\" # CRITICAL: adds a new instance to the cluster to achieve Multi-AZ\n instance_class = \"<INSTANCE_CLASS>\"\n availability_zone = \"<OTHER_AZ>\" # CRITICAL: different AZ ensures Multi-AZ failover\n}\n```"
},
"Recommendation": {
"Text": "Enable Multi-AZ for all DocumentDB Clusters.",
"Url": "https://docs.aws.amazon.com/documentdb/latest/developerguide/failover.html"
"Text": "Enable **Multi-AZ** for DocumentDB and distribute instances across distinct AZs.\n- Maintain at least one replica\n- Set promotion priorities to guide failover\n- Test failover regularly and use resilient client retries\n\nThis builds **fault tolerance** and preserves service availability.",
"Url": "https://hub.prowler.com/check/documentdb_cluster_multi_az_enabled"
}
},
"Categories": [
"redundancy"
"resilience"
],
"DependsOn": [],
"RelatedTo": [],
@@ -1,26 +1,36 @@
{
"Provider": "aws",
"CheckID": "documentdb_cluster_public_snapshot",
"CheckTitle": "Check if DocumentDB manual cluster snapshot is public.",
"CheckType": [],
"CheckTitle": "DocumentDB manual cluster snapshot is not shared publicly",
"CheckType": [
"Software and Configuration Checks/AWS Security Best Practices",
"Software and Configuration Checks/Industry and Regulatory Standards/AWS Foundational Security Best Practices",
"Effects/Data Exposure",
"TTPs/Initial Access"
],
"ServiceName": "documentdb",
"SubServiceName": "",
"ResourceIdTemplate": "arn:partition:service:region:account-id:resource-id",
"ResourceIdTemplate": "",
"Severity": "critical",
"ResourceType": "AwsRdsDbClusterSnapshot",
"Description": "Check if DocumentDB manual cluster snapshot is public.",
"Risk": "If you share an unencrypted manual snapshot as public, the snapshot is available to all AWS accounts. Public snapshots may result in unintended data exposure.",
"RelatedUrl": "https://docs.aws.amazon.com/config/latest/developerguide/docdb-cluster-snapshot-public-prohibited.html",
"Description": "**Amazon DocumentDB** manual cluster snapshot visibility is evaluated to detect snapshots marked as **public** instead of limited to specified AWS accounts.",
"Risk": "**Public snapshots** weaken **confidentiality**: any AWS account can restore and read database contents, enabling data exfiltration.\n\nThey also aid **lateral movement** by revealing embedded secrets/config and reduce accountability when restores occur outside your account.",
"RelatedUrl": "",
"AdditionalURLs": [
"https://docs.aws.amazon.com/documentdb/latest/developerguide/backup_restore-share_cluster_snapshots.html#backup_restore-share_snapshots",
"https://docs.aws.amazon.com/securityhub/latest/userguide/documentdb-controls.html#documentdb-3",
"https://docs.aws.amazon.com/config/latest/developerguide/docdb-cluster-snapshot-public-prohibited.html"
],
"Remediation": {
"Code": {
"CLI": "aws docdb modify-db-snapshot-attribute --db-snapshot-identifier <snapshot_id> --attribute-name restore --values-to-remove all",
"CLI": "aws docdb modify-db-cluster-snapshot-attribute --db-cluster-snapshot-identifier <snapshot_id> --attribute-name restore --values-to-remove all",
"NativeIaC": "",
"Other": "https://docs.aws.amazon.com/securityhub/latest/userguide/documentdb-controls.html#documentdb-3",
"Other": "1. Open the Amazon DocumentDB console and go to Snapshots\n2. Select the public manual cluster snapshot\n3. Click Actions > Share\n4. Set DB snapshot visibility to Private (remove \"all\" if listed)\n5. Click Save",
"Terraform": ""
},
"Recommendation": {
"Text": "To remove public access from a manual snapshot, follow the Sharing a snapshot tutorial.",
"Url": "https://docs.aws.amazon.com/documentdb/latest/developerguide/backup_restore-share_cluster_snapshots.html#backup_restore-share_snapshots"
"Text": "Keep snapshot visibility `Private` and share only with trusted accounts under **least privilege**. Prefer **CMEK encryption** to enforce key-based access and prevent public sharing. Periodically review sharing lists, restrict IAM permissions that alter visibility, and monitor for exposure as **defense in depth**.",
"Url": "https://hub.prowler.com/check/documentdb_cluster_public_snapshot"
}
},
"Categories": [
Loaded 100 of 569 files, more files were not shown because too many files have changed in this diff. Show more