Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 364fd42c48 |
@@ -11,12 +11,6 @@ on:
|
||||
release:
|
||||
types:
|
||||
- 'published'
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
release_tag:
|
||||
description: 'Release tag (e.g., 5.14.0)'
|
||||
required: true
|
||||
type: string
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
@@ -28,7 +22,7 @@ concurrency:
|
||||
env:
|
||||
# Tags
|
||||
LATEST_TAG: latest
|
||||
RELEASE_TAG: ${{ github.event.release.tag_name || inputs.release_tag }}
|
||||
RELEASE_TAG: ${{ github.event.release.tag_name }}
|
||||
STABLE_TAG: stable
|
||||
WORKING_DIRECTORY: ./api
|
||||
|
||||
@@ -78,8 +72,20 @@ jobs:
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1
|
||||
|
||||
- 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 }}:${{ 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' || github.event_name == 'workflow_dispatch'
|
||||
if: github.event_name == 'release'
|
||||
uses: ./.github/actions/slack-notification
|
||||
env:
|
||||
SLACK_CHANNEL_ID: ${{ secrets.SLACK_PLATFORM_DEPLOYMENTS }}
|
||||
@@ -92,21 +98,8 @@ 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 for ${{ matrix.arch }}
|
||||
id: container-push
|
||||
if: github.event_name == 'push' || github.event_name == 'release' || github.event_name == 'workflow_dispatch'
|
||||
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 }}:${{ 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 completed
|
||||
if: (github.event_name == 'release' || github.event_name == 'workflow_dispatch') && always()
|
||||
if: github.event_name == 'release' && always()
|
||||
uses: ./.github/actions/slack-notification
|
||||
env:
|
||||
SLACK_CHANNEL_ID: ${{ secrets.SLACK_PLATFORM_DEPLOYMENTS }}
|
||||
@@ -123,7 +116,7 @@ jobs:
|
||||
# Create and push multi-architecture manifest
|
||||
create-manifest:
|
||||
needs: [setup, container-build-push]
|
||||
if: github.event_name == 'push' || github.event_name == 'release' || github.event_name == 'workflow_dispatch'
|
||||
if: github.event_name == 'push' || github.event_name == 'release'
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
@@ -146,7 +139,7 @@ jobs:
|
||||
${{ 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' || github.event_name == 'workflow_dispatch'
|
||||
if: github.event_name == 'release'
|
||||
run: |
|
||||
docker buildx imagetools create \
|
||||
-t ${{ env.PROWLERCLOUD_DOCKERHUB_REPOSITORY }}/${{ env.PROWLERCLOUD_DOCKERHUB_IMAGE }}:${{ env.RELEASE_TAG }} \
|
||||
|
||||
@@ -10,12 +10,6 @@ on:
|
||||
release:
|
||||
types:
|
||||
- 'published'
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
release_tag:
|
||||
description: 'Release tag (e.g., 5.14.0)'
|
||||
required: true
|
||||
type: string
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
@@ -27,7 +21,7 @@ concurrency:
|
||||
env:
|
||||
# Tags
|
||||
LATEST_TAG: latest
|
||||
RELEASE_TAG: ${{ github.event.release.tag_name || inputs.release_tag }}
|
||||
RELEASE_TAG: ${{ github.event.release.tag_name }}
|
||||
STABLE_TAG: stable
|
||||
WORKING_DIRECTORY: ./mcp_server
|
||||
|
||||
@@ -49,7 +43,7 @@ jobs:
|
||||
|
||||
container-build-push:
|
||||
needs: setup
|
||||
runs-on: ${{ matrix.runner }}
|
||||
runs-on: ${{ matrix.runner }}
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
@@ -76,23 +70,8 @@ jobs:
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1
|
||||
|
||||
- name: Notify container push started
|
||||
if: github.event_name == 'release' || github.event_name == 'workflow_dispatch'
|
||||
uses: ./.github/actions/slack-notification
|
||||
env:
|
||||
SLACK_CHANNEL_ID: ${{ secrets.SLACK_PLATFORM_DEPLOYMENTS }}
|
||||
COMPONENT: MCP
|
||||
RELEASE_TAG: ${{ env.RELEASE_TAG }}
|
||||
GITHUB_SERVER_URL: ${{ github.server_url }}
|
||||
GITHUB_REPOSITORY: ${{ github.repository }}
|
||||
GITHUB_RUN_ID: ${{ github.run_id }}
|
||||
with:
|
||||
slack-bot-token: ${{ secrets.SLACK_BOT_TOKEN }}
|
||||
payload-file-path: "./.github/scripts/slack-messages/container-release-started.json"
|
||||
|
||||
- name: Build and push MCP container for ${{ matrix.arch }}
|
||||
id: container-push
|
||||
if: github.event_name == 'push' || github.event_name == 'release' || github.event_name == 'workflow_dispatch'
|
||||
if: github.event_name == 'push' || github.event_name == 'release'
|
||||
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0
|
||||
with:
|
||||
context: ${{ env.WORKING_DIRECTORY }}
|
||||
@@ -111,8 +90,22 @@ jobs:
|
||||
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'
|
||||
uses: ./.github/actions/slack-notification
|
||||
env:
|
||||
SLACK_CHANNEL_ID: ${{ secrets.SLACK_PLATFORM_DEPLOYMENTS }}
|
||||
COMPONENT: MCP
|
||||
RELEASE_TAG: ${{ env.RELEASE_TAG }}
|
||||
GITHUB_SERVER_URL: ${{ github.server_url }}
|
||||
GITHUB_REPOSITORY: ${{ github.repository }}
|
||||
GITHUB_RUN_ID: ${{ github.run_id }}
|
||||
with:
|
||||
slack-bot-token: ${{ secrets.SLACK_BOT_TOKEN }}
|
||||
payload-file-path: "./.github/scripts/slack-messages/container-release-started.json"
|
||||
|
||||
- name: Notify container push completed
|
||||
if: (github.event_name == 'release' || github.event_name == 'workflow_dispatch') && always()
|
||||
if: github.event_name == 'release' && always()
|
||||
uses: ./.github/actions/slack-notification
|
||||
env:
|
||||
SLACK_CHANNEL_ID: ${{ secrets.SLACK_PLATFORM_DEPLOYMENTS }}
|
||||
@@ -129,7 +122,7 @@ jobs:
|
||||
# Create and push multi-architecture manifest
|
||||
create-manifest:
|
||||
needs: [setup, container-build-push]
|
||||
if: github.event_name == 'push' || github.event_name == 'release' || github.event_name == 'workflow_dispatch'
|
||||
if: github.event_name == 'push' || github.event_name == 'release'
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
@@ -152,14 +145,14 @@ jobs:
|
||||
${{ 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' || github.event_name == 'workflow_dispatch'
|
||||
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
|
||||
|
||||
@@ -16,12 +16,6 @@ on:
|
||||
release:
|
||||
types:
|
||||
- 'published'
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
release_tag:
|
||||
description: 'Release tag (e.g., 5.14.0)'
|
||||
required: true
|
||||
type: string
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
@@ -52,7 +46,7 @@ env:
|
||||
jobs:
|
||||
container-build-push:
|
||||
if: github.repository == 'prowler-cloud/prowler'
|
||||
runs-on: ${{ matrix.runner }}
|
||||
runs-on: ${{ matrix.runner }}
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
@@ -147,23 +141,8 @@ jobs:
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1
|
||||
|
||||
- name: Notify container push started
|
||||
if: github.event_name == 'release' || github.event_name == 'workflow_dispatch'
|
||||
uses: ./.github/actions/slack-notification
|
||||
env:
|
||||
SLACK_CHANNEL_ID: ${{ secrets.SLACK_PLATFORM_DEPLOYMENTS }}
|
||||
COMPONENT: SDK
|
||||
RELEASE_TAG: ${{ env.PROWLER_VERSION }}
|
||||
GITHUB_SERVER_URL: ${{ github.server_url }}
|
||||
GITHUB_REPOSITORY: ${{ github.repository }}
|
||||
GITHUB_RUN_ID: ${{ github.run_id }}
|
||||
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 for ${{ matrix.arch }}
|
||||
id: container-push
|
||||
if: github.event_name == 'push' || github.event_name == 'release' || github.event_name == 'workflow_dispatch'
|
||||
if: github.event_name == 'push' || github.event_name == 'release'
|
||||
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0
|
||||
with:
|
||||
context: .
|
||||
@@ -175,8 +154,22 @@ jobs:
|
||||
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'
|
||||
uses: ./.github/actions/slack-notification
|
||||
env:
|
||||
SLACK_CHANNEL_ID: ${{ secrets.SLACK_PLATFORM_DEPLOYMENTS }}
|
||||
COMPONENT: SDK
|
||||
RELEASE_TAG: ${{ env.PROWLER_VERSION }}
|
||||
GITHUB_SERVER_URL: ${{ github.server_url }}
|
||||
GITHUB_REPOSITORY: ${{ github.repository }}
|
||||
GITHUB_RUN_ID: ${{ github.run_id }}
|
||||
with:
|
||||
slack-bot-token: ${{ secrets.SLACK_BOT_TOKEN }}
|
||||
payload-file-path: "./.github/scripts/slack-messages/container-release-started.json"
|
||||
|
||||
- name: Notify container push completed
|
||||
if: (github.event_name == 'release' || github.event_name == 'workflow_dispatch') && always()
|
||||
if: github.event_name == 'release' && always()
|
||||
uses: ./.github/actions/slack-notification
|
||||
env:
|
||||
SLACK_CHANNEL_ID: ${{ secrets.SLACK_PLATFORM_DEPLOYMENTS }}
|
||||
@@ -193,7 +186,7 @@ jobs:
|
||||
# Create and push multi-architecture manifest
|
||||
create-manifest:
|
||||
needs: [container-build-push]
|
||||
if: github.event_name == 'push' || github.event_name == 'release' || github.event_name == 'workflow_dispatch'
|
||||
if: github.event_name == 'push' || github.event_name == 'release'
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
@@ -203,15 +196,6 @@ jobs:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Login to Public ECR
|
||||
uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0
|
||||
with:
|
||||
registry: public.ecr.aws
|
||||
username: ${{ secrets.PUBLIC_ECR_AWS_ACCESS_KEY_ID }}
|
||||
password: ${{ secrets.PUBLIC_ECR_AWS_SECRET_ACCESS_KEY }}
|
||||
env:
|
||||
AWS_REGION: ${{ env.AWS_REGION }}
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1
|
||||
|
||||
@@ -226,13 +210,13 @@ jobs:
|
||||
${{ 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' || github.event_name == 'workflow_dispatch'
|
||||
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 ${{ 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 \
|
||||
|
||||
@@ -10,12 +10,6 @@ on:
|
||||
release:
|
||||
types:
|
||||
- 'published'
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
release_tag:
|
||||
description: 'Release tag (e.g., 5.14.0)'
|
||||
required: true
|
||||
type: string
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
@@ -27,7 +21,7 @@ concurrency:
|
||||
env:
|
||||
# Tags
|
||||
LATEST_TAG: latest
|
||||
RELEASE_TAG: ${{ github.event.release.tag_name || inputs.release_tag }}
|
||||
RELEASE_TAG: ${{ github.event.release.tag_name }}
|
||||
STABLE_TAG: stable
|
||||
WORKING_DIRECTORY: ./ui
|
||||
|
||||
@@ -52,7 +46,7 @@ jobs:
|
||||
|
||||
container-build-push:
|
||||
needs: setup
|
||||
runs-on: ${{ matrix.runner }}
|
||||
runs-on: ${{ matrix.runner }}
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
@@ -80,8 +74,37 @@ jobs:
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1
|
||||
|
||||
- name: Build and push UI container for ${{ matrix.arch }}
|
||||
if: github.event_name == 'push'
|
||||
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0
|
||||
with:
|
||||
context: ${{ env.WORKING_DIRECTORY }}
|
||||
build-args: |
|
||||
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 }}:${{ needs.setup.outputs.short-sha }}
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
|
||||
- name: Notify container push started
|
||||
if: github.event_name == 'release' || github.event_name == 'workflow_dispatch'
|
||||
if: github.event_name == 'release'
|
||||
uses: ./.github/actions/slack-notification
|
||||
env:
|
||||
SLACK_CHANNEL_ID: ${{ secrets.SLACK_PLATFORM_DEPLOYMENTS }}
|
||||
@@ -94,24 +117,8 @@ 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 for ${{ matrix.arch }}
|
||||
id: container-push
|
||||
if: github.event_name == 'push' || github.event_name == 'release' || github.event_name == 'workflow_dispatch'
|
||||
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' || github.event_name == 'workflow_dispatch') && format('v{0}', env.RELEASE_TAG) || 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: Notify container push completed
|
||||
if: (github.event_name == 'release' || github.event_name == 'workflow_dispatch') && always()
|
||||
if: github.event_name == 'release' && always()
|
||||
uses: ./.github/actions/slack-notification
|
||||
env:
|
||||
SLACK_CHANNEL_ID: ${{ secrets.SLACK_PLATFORM_DEPLOYMENTS }}
|
||||
@@ -128,7 +135,7 @@ jobs:
|
||||
# Create and push multi-architecture manifest
|
||||
create-manifest:
|
||||
needs: [setup, container-build-push]
|
||||
if: github.event_name == 'push' || github.event_name == 'release' || github.event_name == 'workflow_dispatch'
|
||||
if: github.event_name == 'push' || github.event_name == 'release'
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
@@ -151,7 +158,7 @@ jobs:
|
||||
${{ 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' || github.event_name == 'workflow_dispatch'
|
||||
if: github.event_name == 'release'
|
||||
run: |
|
||||
docker buildx imagetools create \
|
||||
-t ${{ env.PROWLERCLOUD_DOCKERHUB_REPOSITORY }}/${{ env.PROWLERCLOUD_DOCKERHUB_IMAGE }}:${{ env.RELEASE_TAG }} \
|
||||
|
||||
@@ -126,12 +126,3 @@ repos:
|
||||
entry: bash -c 'vulture --exclude "contrib,.venv,api/src/backend/api/tests/,api/src/backend/conftest.py,api/src/backend/tasks/tests/" --min-confidence 100 .'
|
||||
language: system
|
||||
files: '.*\.py'
|
||||
|
||||
- id: ui-checks
|
||||
name: UI - Husky Pre-commit
|
||||
description: "Run UI pre-commit checks (Claude Code validation + healthcheck)"
|
||||
entry: bash -c 'cd ui && .husky/pre-commit'
|
||||
language: system
|
||||
files: '^ui/.*\.(ts|tsx|js|jsx|json|css)$'
|
||||
pass_filenames: false
|
||||
verbose: true
|
||||
|
||||
@@ -2,25 +2,7 @@
|
||||
|
||||
All notable changes to the **Prowler API** are documented in this file.
|
||||
|
||||
|
||||
## [1.15.2] (Prowler v5.14.2)
|
||||
|
||||
### Fixed
|
||||
- Unique constraint violation during compliance overviews task [(#9436)](https://github.com/prowler-cloud/prowler/pull/9436)
|
||||
- Division by zero error in ENS PDF report when all requirements are manual [(#9443)](https://github.com/prowler-cloud/prowler/pull/9443)
|
||||
|
||||
---
|
||||
|
||||
## [1.15.1] (Prowler v5.14.1)
|
||||
|
||||
### Fixed
|
||||
- Typo in PDF reporting [(#9322)](https://github.com/prowler-cloud/prowler/pull/9322)
|
||||
- IaC provider initialization failure when mutelist processor is configured [(#9331)](https://github.com/prowler-cloud/prowler/pull/9331)
|
||||
- Match logic for ThreatScore when counting findings [(#9348)](https://github.com/prowler-cloud/prowler/pull/9348)
|
||||
|
||||
---
|
||||
|
||||
## [1.15.0] (Prowler v5.14.0)
|
||||
## [1.15.0] (Prowler UNRELEASED)
|
||||
|
||||
### Added
|
||||
- IaC (Infrastructure as Code) provider support for remote repositories [(#8751)](https://github.com/prowler-cloud/prowler/pull/8751)
|
||||
@@ -32,33 +14,28 @@ 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)
|
||||
- Support PDF reporting for ENS compliance framework [(#9158)](https://github.com/prowler-cloud/prowler/pull/9158)
|
||||
- Support PDF reporting for NIS2 compliance framework [(#9170)](https://github.com/prowler-cloud/prowler/pull/9170)
|
||||
- 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)
|
||||
- Support Prowler ThreatScore for the K8S provider [(#9235)](https://github.com/prowler-cloud/prowler/pull/9235)
|
||||
- Enhanced compliance overview endpoint with provider filtering and latest scan aggregation [(#9244)](https://github.com/prowler-cloud/prowler/pull/9244)
|
||||
- New endpoint `GET /api/v1/overview/regions` to retrieve aggregated findings data by region [(#9273)](https://github.com/prowler-cloud/prowler/pull/9273)
|
||||
|
||||
### 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)
|
||||
- Updated 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)
|
||||
- Removed compliance generation for providers without compliance frameworks [(#9208)](https://github.com/prowler-cloud/prowler/pull/9208)
|
||||
- Refresh output report timestamps for each scan [(#9272)](https://github.com/prowler-cloud/prowler/pull/9272)
|
||||
- Severity overview endpoint now ignores muted findings as expected [(#9283)](https://github.com/prowler-cloud/prowler/pull/9283)
|
||||
- Fixed discrepancy between ThreatScore PDF report values and database calculations [(#9296)](https://github.com/prowler-cloud/prowler/pull/9296)
|
||||
|
||||
### 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.1] (Prowler v5.13.1)
|
||||
## [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
|
||||
- `/api/v1/overviews/providers` collapses data by provider type so the UI receives a single aggregated record per cloud family even when multiple accounts exist [(#9053)](https://github.com/prowler-cloud/prowler/pull/9053)
|
||||
@@ -67,7 +44,7 @@ All notable changes to the **Prowler API** are documented in this file.
|
||||
|
||||
---
|
||||
|
||||
## [1.14.0] (Prowler v5.13.0)
|
||||
## [1.14.0] (Prowler 5.13.0)
|
||||
|
||||
### Added
|
||||
- Default JWT keys are generated and stored if they are missing from configuration [(#8655)](https://github.com/prowler-cloud/prowler/pull/8655)
|
||||
@@ -91,14 +68,14 @@ All notable changes to the **Prowler API** are documented in this file.
|
||||
|
||||
---
|
||||
|
||||
## [1.13.2] (Prowler v5.12.3)
|
||||
## [1.13.2] (Prowler 5.12.3)
|
||||
|
||||
### Fixed
|
||||
- 500 error when deleting user [(#8731)](https://github.com/prowler-cloud/prowler/pull/8731)
|
||||
|
||||
---
|
||||
|
||||
## [1.13.1] (Prowler v5.12.2)
|
||||
## [1.13.1] (Prowler 5.12.2)
|
||||
|
||||
### Changed
|
||||
- Renamed compliance overview task queue to `compliance` [(#8755)](https://github.com/prowler-cloud/prowler/pull/8755)
|
||||
@@ -108,7 +85,7 @@ All notable changes to the **Prowler API** are documented in this file.
|
||||
|
||||
---
|
||||
|
||||
## [1.13.0] (Prowler v5.12.0)
|
||||
## [1.13.0] (Prowler 5.12.0)
|
||||
|
||||
### Added
|
||||
- Integration with JIRA, enabling sending findings to a JIRA project [(#8622)](https://github.com/prowler-cloud/prowler/pull/8622), [(#8637)](https://github.com/prowler-cloud/prowler/pull/8637)
|
||||
@@ -117,7 +94,7 @@ All notable changes to the **Prowler API** are documented in this file.
|
||||
|
||||
---
|
||||
|
||||
## [1.12.0] (Prowler v5.11.0)
|
||||
## [1.12.0] (Prowler 5.11.0)
|
||||
|
||||
### Added
|
||||
- Lighthouse support for OpenAI GPT-5 [(#8527)](https://github.com/prowler-cloud/prowler/pull/8527)
|
||||
@@ -129,7 +106,7 @@ All notable changes to the **Prowler API** are documented in this file.
|
||||
|
||||
---
|
||||
|
||||
## [1.11.0] (Prowler v5.10.0)
|
||||
## [1.11.0] (Prowler 5.10.0)
|
||||
|
||||
### Added
|
||||
- Github provider support [(#8271)](https://github.com/prowler-cloud/prowler/pull/8271)
|
||||
|
||||
@@ -610,24 +610,6 @@ 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"
|
||||
@@ -1182,18 +1164,6 @@ files = [
|
||||
{file = "charset_normalizer-3.4.3.tar.gz", hash = "sha256:6fce4b8500244f6fcb71465d4a4930d132ba9ab8e71a7859e6a5d59851068d14"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "circuitbreaker"
|
||||
version = "2.1.3"
|
||||
description = "Python Circuit Breaker pattern implementation"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "circuitbreaker-2.1.3-py3-none-any.whl", hash = "sha256:87ba6a3ed03fdc7032bc175561c2b04d52ade9d5faf94ca2b035fbdc5e6b1dd1"},
|
||||
{file = "circuitbreaker-2.1.3.tar.gz", hash = "sha256:1a4baee510f7bea3c91b194dcce7c07805fe96c4423ed5594b75af438531d084"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "click"
|
||||
version = "8.2.1"
|
||||
@@ -2468,72 +2438,6 @@ files = [
|
||||
{file = "frozenlist-1.7.0.tar.gz", hash = "sha256:2e310d81923c2437ea8670467121cc3e9b0f76d3043cc1d2331d56c7fb7a3a8f"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "gevent"
|
||||
version = "25.9.1"
|
||||
description = "Coroutine-based network library"
|
||||
optional = false
|
||||
python-versions = ">=3.9"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "gevent-25.9.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:856b990be5590e44c3a3dc6c8d48a40eaccbb42e99d2b791d11d1e7711a4297e"},
|
||||
{file = "gevent-25.9.1-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:fe1599d0b30e6093eb3213551751b24feeb43db79f07e89d98dd2f3330c9063e"},
|
||||
{file = "gevent-25.9.1-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:f0d8b64057b4bf1529b9ef9bd2259495747fba93d1f836c77bfeaacfec373fd0"},
|
||||
{file = "gevent-25.9.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b56cbc820e3136ba52cd690bdf77e47a4c239964d5f80dc657c1068e0fe9521c"},
|
||||
{file = "gevent-25.9.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:c5fa9ce5122c085983e33e0dc058f81f5264cebe746de5c401654ab96dddfca8"},
|
||||
{file = "gevent-25.9.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:03c74fec58eda4b4edc043311fca8ba4f8744ad1632eb0a41d5ec25413581975"},
|
||||
{file = "gevent-25.9.1-cp310-cp310-win_amd64.whl", hash = "sha256:a8ae9f895e8651d10b0a8328a61c9c53da11ea51b666388aa99b0ce90f9fdc27"},
|
||||
{file = "gevent-25.9.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:18e5aff9e8342dc954adb9c9c524db56c2f3557999463445ba3d9cbe3dada7b7"},
|
||||
{file = "gevent-25.9.1-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:1cdf6db28f050ee103441caa8b0448ace545364f775059d5e2de089da975c457"},
|
||||
{file = "gevent-25.9.1-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:812debe235a8295be3b2a63b136c2474241fa5c58af55e6a0f8cfc29d4936235"},
|
||||
{file = "gevent-25.9.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b28b61ff9216a3d73fe8f35669eefcafa957f143ac534faf77e8a19eb9e6883a"},
|
||||
{file = "gevent-25.9.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:5e4b6278b37373306fc6b1e5f0f1cf56339a1377f67c35972775143d8d7776ff"},
|
||||
{file = "gevent-25.9.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d99f0cb2ce43c2e8305bf75bee61a8bde06619d21b9d0316ea190fc7a0620a56"},
|
||||
{file = "gevent-25.9.1-cp311-cp311-win_amd64.whl", hash = "sha256:72152517ecf548e2f838c61b4be76637d99279dbaa7e01b3924df040aa996586"},
|
||||
{file = "gevent-25.9.1-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:46b188248c84ffdec18a686fcac5dbb32365d76912e14fda350db5dc0bfd4f86"},
|
||||
{file = "gevent-25.9.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f2b54ea3ca6f0c763281cd3f96010ac7e98c2e267feb1221b5a26e2ca0b9a692"},
|
||||
{file = "gevent-25.9.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:7a834804ac00ed8a92a69d3826342c677be651b1c3cd66cc35df8bc711057aa2"},
|
||||
{file = "gevent-25.9.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:323a27192ec4da6b22a9e51c3d9d896ff20bc53fdc9e45e56eaab76d1c39dd74"},
|
||||
{file = "gevent-25.9.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:6ea78b39a2c51d47ff0f130f4c755a9a4bbb2dd9721149420ad4712743911a51"},
|
||||
{file = "gevent-25.9.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:dc45cd3e1cc07514a419960af932a62eb8515552ed004e56755e4bf20bad30c5"},
|
||||
{file = "gevent-25.9.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:34e01e50c71eaf67e92c186ee0196a039d6e4f4b35670396baed4a2d8f1b347f"},
|
||||
{file = "gevent-25.9.1-cp312-cp312-win_amd64.whl", hash = "sha256:4acd6bcd5feabf22c7c5174bd3b9535ee9f088d2bbce789f740ad8d6554b18f3"},
|
||||
{file = "gevent-25.9.1-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:4f84591d13845ee31c13f44bdf6bd6c3dbf385b5af98b2f25ec328213775f2ed"},
|
||||
{file = "gevent-25.9.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:9cdbb24c276a2d0110ad5c978e49daf620b153719ac8a548ce1250a7eb1b9245"},
|
||||
{file = "gevent-25.9.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:88b6c07169468af631dcf0fdd3658f9246d6822cc51461d43f7c44f28b0abb82"},
|
||||
{file = "gevent-25.9.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b7bb0e29a7b3e6ca9bed2394aa820244069982c36dc30b70eb1004dd67851a48"},
|
||||
{file = "gevent-25.9.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2951bb070c0ee37b632ac9134e4fdaad70d2e660c931bb792983a0837fe5b7d7"},
|
||||
{file = "gevent-25.9.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e4e17c2d57e9a42e25f2a73d297b22b60b2470a74be5a515b36c984e1a246d47"},
|
||||
{file = "gevent-25.9.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8d94936f8f8b23d9de2251798fcb603b84f083fdf0d7f427183c1828fb64f117"},
|
||||
{file = "gevent-25.9.1-cp313-cp313-win_amd64.whl", hash = "sha256:eb51c5f9537b07da673258b4832f6635014fee31690c3f0944d34741b69f92fa"},
|
||||
{file = "gevent-25.9.1-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:1a3fe4ea1c312dbf6b375b416925036fe79a40054e6bf6248ee46526ea628be1"},
|
||||
{file = "gevent-25.9.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:0adb937f13e5fb90cca2edf66d8d7e99d62a299687400ce2edee3f3504009356"},
|
||||
{file = "gevent-25.9.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:427f869a2050a4202d93cf7fd6ab5cffb06d3e9113c10c967b6e2a0d45237cb8"},
|
||||
{file = "gevent-25.9.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:c049880175e8c93124188f9d926af0a62826a3b81aa6d3074928345f8238279e"},
|
||||
{file = "gevent-25.9.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b5a67a0974ad9f24721034d1e008856111e0535f1541499f72a733a73d658d1c"},
|
||||
{file = "gevent-25.9.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:1d0f5d8d73f97e24ea8d24d8be0f51e0cf7c54b8021c1fddb580bf239474690f"},
|
||||
{file = "gevent-25.9.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ddd3ff26e5c4240d3fbf5516c2d9d5f2a998ef87cfb73e1429cfaeaaec860fa6"},
|
||||
{file = "gevent-25.9.1-cp314-cp314-win_amd64.whl", hash = "sha256:bb63c0d6cb9950cc94036a4995b9cc4667b8915366613449236970f4394f94d7"},
|
||||
{file = "gevent-25.9.1-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f18f80aef6b1f6907219affe15b36677904f7cfeed1f6a6bc198616e507ae2d7"},
|
||||
{file = "gevent-25.9.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:b274a53e818124a281540ebb4e7a2c524778f745b7a99b01bdecf0ca3ac0ddb0"},
|
||||
{file = "gevent-25.9.1-cp39-cp39-win32.whl", hash = "sha256:c6c91f7e33c7f01237755884316110ee7ea076f5bdb9aa0982b6dc63243c0a38"},
|
||||
{file = "gevent-25.9.1-cp39-cp39-win_amd64.whl", hash = "sha256:012a44b0121f3d7c800740ff80351c897e85e76a7e4764690f35c5ad9ec17de5"},
|
||||
{file = "gevent-25.9.1.tar.gz", hash = "sha256:adf9cd552de44a4e6754c51ff2e78d9193b7fa6eab123db9578a210e657235dd"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
cffi = {version = ">=1.17.1", markers = "platform_python_implementation == \"CPython\" and sys_platform == \"win32\""}
|
||||
greenlet = {version = ">=3.2.2", markers = "platform_python_implementation == \"CPython\""}
|
||||
"zope.event" = "*"
|
||||
"zope.interface" = "*"
|
||||
|
||||
[package.extras]
|
||||
dnspython = ["dnspython (>=1.16.0,<2.0) ; python_version < \"3.10\"", "idna ; python_version < \"3.10\""]
|
||||
docs = ["furo", "repoze.sphinx.autointerface", "sphinx", "sphinxcontrib-programoutput", "zope.schema"]
|
||||
monitor = ["psutil (>=5.7.0) ; sys_platform != \"win32\" or platform_python_implementation == \"CPython\""]
|
||||
recommended = ["cffi (>=1.17.1) ; platform_python_implementation == \"CPython\"", "dnspython (>=1.16.0,<2.0) ; python_version < \"3.10\"", "idna ; python_version < \"3.10\"", "psutil (>=5.7.0) ; sys_platform != \"win32\" or platform_python_implementation == \"CPython\""]
|
||||
test = ["cffi (>=1.17.1) ; platform_python_implementation == \"CPython\"", "coverage (>=5.0) ; sys_platform != \"win32\"", "dnspython (>=1.16.0,<2.0) ; python_version < \"3.10\"", "idna ; python_version < \"3.10\"", "objgraph", "psutil (>=5.7.0) ; sys_platform != \"win32\" or platform_python_implementation == \"CPython\"", "requests"]
|
||||
|
||||
[[package]]
|
||||
name = "google-api-core"
|
||||
version = "2.25.1"
|
||||
@@ -2667,87 +2571,6 @@ files = [
|
||||
dev = ["pytest"]
|
||||
docs = ["sphinx", "sphinx-autobuild"]
|
||||
|
||||
[[package]]
|
||||
name = "greenlet"
|
||||
version = "3.2.4"
|
||||
description = "Lightweight in-process concurrent programming"
|
||||
optional = false
|
||||
python-versions = ">=3.9"
|
||||
groups = ["main"]
|
||||
markers = "platform_python_implementation == \"CPython\""
|
||||
files = [
|
||||
{file = "greenlet-3.2.4-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:8c68325b0d0acf8d91dde4e6f930967dd52a5302cd4062932a6b2e7c2969f47c"},
|
||||
{file = "greenlet-3.2.4-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:94385f101946790ae13da500603491f04a76b6e4c059dab271b3ce2e283b2590"},
|
||||
{file = "greenlet-3.2.4-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f10fd42b5ee276335863712fa3da6608e93f70629c631bf77145021600abc23c"},
|
||||
{file = "greenlet-3.2.4-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:c8c9e331e58180d0d83c5b7999255721b725913ff6bc6cf39fa2a45841a4fd4b"},
|
||||
{file = "greenlet-3.2.4-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:58b97143c9cc7b86fc458f215bd0932f1757ce649e05b640fea2e79b54cedb31"},
|
||||
{file = "greenlet-3.2.4-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c2ca18a03a8cfb5b25bc1cbe20f3d9a4c80d8c3b13ba3df49ac3961af0b1018d"},
|
||||
{file = "greenlet-3.2.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:9fe0a28a7b952a21e2c062cd5756d34354117796c6d9215a87f55e38d15402c5"},
|
||||
{file = "greenlet-3.2.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:8854167e06950ca75b898b104b63cc646573aa5fef1353d4508ecdd1ee76254f"},
|
||||
{file = "greenlet-3.2.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:f47617f698838ba98f4ff4189aef02e7343952df3a615f847bb575c3feb177a7"},
|
||||
{file = "greenlet-3.2.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:af41be48a4f60429d5cad9d22175217805098a9ef7c40bfef44f7669fb9d74d8"},
|
||||
{file = "greenlet-3.2.4-cp310-cp310-win_amd64.whl", hash = "sha256:73f49b5368b5359d04e18d15828eecc1806033db5233397748f4ca813ff1056c"},
|
||||
{file = "greenlet-3.2.4-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:96378df1de302bc38e99c3a9aa311967b7dc80ced1dcc6f171e99842987882a2"},
|
||||
{file = "greenlet-3.2.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:1ee8fae0519a337f2329cb78bd7a8e128ec0f881073d43f023c7b8d4831d5246"},
|
||||
{file = "greenlet-3.2.4-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:94abf90142c2a18151632371140b3dba4dee031633fe614cb592dbb6c9e17bc3"},
|
||||
{file = "greenlet-3.2.4-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:4d1378601b85e2e5171b99be8d2dc85f594c79967599328f95c1dc1a40f1c633"},
|
||||
{file = "greenlet-3.2.4-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0db5594dce18db94f7d1650d7489909b57afde4c580806b8d9203b6e79cdc079"},
|
||||
{file = "greenlet-3.2.4-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2523e5246274f54fdadbce8494458a2ebdcdbc7b802318466ac5606d3cded1f8"},
|
||||
{file = "greenlet-3.2.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:1987de92fec508535687fb807a5cea1560f6196285a4cde35c100b8cd632cc52"},
|
||||
{file = "greenlet-3.2.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:55e9c5affaa6775e2c6b67659f3a71684de4c549b3dd9afca3bc773533d284fa"},
|
||||
{file = "greenlet-3.2.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c9c6de1940a7d828635fbd254d69db79e54619f165ee7ce32fda763a9cb6a58c"},
|
||||
{file = "greenlet-3.2.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:03c5136e7be905045160b1b9fdca93dd6727b180feeafda6818e6496434ed8c5"},
|
||||
{file = "greenlet-3.2.4-cp311-cp311-win_amd64.whl", hash = "sha256:9c40adce87eaa9ddb593ccb0fa6a07caf34015a29bf8d344811665b573138db9"},
|
||||
{file = "greenlet-3.2.4-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:3b67ca49f54cede0186854a008109d6ee71f66bd57bb36abd6d0a0267b540cdd"},
|
||||
{file = "greenlet-3.2.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ddf9164e7a5b08e9d22511526865780a576f19ddd00d62f8a665949327fde8bb"},
|
||||
{file = "greenlet-3.2.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f28588772bb5fb869a8eb331374ec06f24a83a9c25bfa1f38b6993afe9c1e968"},
|
||||
{file = "greenlet-3.2.4-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:5c9320971821a7cb77cfab8d956fa8e39cd07ca44b6070db358ceb7f8797c8c9"},
|
||||
{file = "greenlet-3.2.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c60a6d84229b271d44b70fb6e5fa23781abb5d742af7b808ae3f6efd7c9c60f6"},
|
||||
{file = "greenlet-3.2.4-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3b3812d8d0c9579967815af437d96623f45c0f2ae5f04e366de62a12d83a8fb0"},
|
||||
{file = "greenlet-3.2.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:abbf57b5a870d30c4675928c37278493044d7c14378350b3aa5d484fa65575f0"},
|
||||
{file = "greenlet-3.2.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:20fb936b4652b6e307b8f347665e2c615540d4b42b3b4c8a321d8286da7e520f"},
|
||||
{file = "greenlet-3.2.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ee7a6ec486883397d70eec05059353b8e83eca9168b9f3f9a361971e77e0bcd0"},
|
||||
{file = "greenlet-3.2.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:326d234cbf337c9c3def0676412eb7040a35a768efc92504b947b3e9cfc7543d"},
|
||||
{file = "greenlet-3.2.4-cp312-cp312-win_amd64.whl", hash = "sha256:a7d4e128405eea3814a12cc2605e0e6aedb4035bf32697f72deca74de4105e02"},
|
||||
{file = "greenlet-3.2.4-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:1a921e542453fe531144e91e1feedf12e07351b1cf6c9e8a3325ea600a715a31"},
|
||||
{file = "greenlet-3.2.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:cd3c8e693bff0fff6ba55f140bf390fa92c994083f838fece0f63be121334945"},
|
||||
{file = "greenlet-3.2.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:710638eb93b1fa52823aa91bf75326f9ecdfd5e0466f00789246a5280f4ba0fc"},
|
||||
{file = "greenlet-3.2.4-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:c5111ccdc9c88f423426df3fd1811bfc40ed66264d35aa373420a34377efc98a"},
|
||||
{file = "greenlet-3.2.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d76383238584e9711e20ebe14db6c88ddcedc1829a9ad31a584389463b5aa504"},
|
||||
{file = "greenlet-3.2.4-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:23768528f2911bcd7e475210822ffb5254ed10d71f4028387e5a99b4c6699671"},
|
||||
{file = "greenlet-3.2.4-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:00fadb3fedccc447f517ee0d3fd8fe49eae949e1cd0f6a611818f4f6fb7dc83b"},
|
||||
{file = "greenlet-3.2.4-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:d25c5091190f2dc0eaa3f950252122edbbadbb682aa7b1ef2f8af0f8c0afefae"},
|
||||
{file = "greenlet-3.2.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6e343822feb58ac4d0a1211bd9399de2b3a04963ddeec21530fc426cc121f19b"},
|
||||
{file = "greenlet-3.2.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ca7f6f1f2649b89ce02f6f229d7c19f680a6238af656f61e0115b24857917929"},
|
||||
{file = "greenlet-3.2.4-cp313-cp313-win_amd64.whl", hash = "sha256:554b03b6e73aaabec3745364d6239e9e012d64c68ccd0b8430c64ccc14939a8b"},
|
||||
{file = "greenlet-3.2.4-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:49a30d5fda2507ae77be16479bdb62a660fa51b1eb4928b524975b3bde77b3c0"},
|
||||
{file = "greenlet-3.2.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:299fd615cd8fc86267b47597123e3f43ad79c9d8a22bebdce535e53550763e2f"},
|
||||
{file = "greenlet-3.2.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:c17b6b34111ea72fc5a4e4beec9711d2226285f0386ea83477cbb97c30a3f3a5"},
|
||||
{file = "greenlet-3.2.4-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b4a1870c51720687af7fa3e7cda6d08d801dae660f75a76f3845b642b4da6ee1"},
|
||||
{file = "greenlet-3.2.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:061dc4cf2c34852b052a8620d40f36324554bc192be474b9e9770e8c042fd735"},
|
||||
{file = "greenlet-3.2.4-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:44358b9bf66c8576a9f57a590d5f5d6e72fa4228b763d0e43fee6d3b06d3a337"},
|
||||
{file = "greenlet-3.2.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:2917bdf657f5859fbf3386b12d68ede4cf1f04c90c3a6bc1f013dd68a22e2269"},
|
||||
{file = "greenlet-3.2.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:015d48959d4add5d6c9f6c5210ee3803a830dce46356e3bc326d6776bde54681"},
|
||||
{file = "greenlet-3.2.4-cp314-cp314-win_amd64.whl", hash = "sha256:e37ab26028f12dbb0ff65f29a8d3d44a765c61e729647bf2ddfbbed621726f01"},
|
||||
{file = "greenlet-3.2.4-cp39-cp39-macosx_11_0_universal2.whl", hash = "sha256:b6a7c19cf0d2742d0809a4c05975db036fdff50cd294a93632d6a310bf9ac02c"},
|
||||
{file = "greenlet-3.2.4-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:27890167f55d2387576d1f41d9487ef171849ea0359ce1510ca6e06c8bece11d"},
|
||||
{file = "greenlet-3.2.4-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:18d9260df2b5fbf41ae5139e1be4e796d99655f023a636cd0e11e6406cca7d58"},
|
||||
{file = "greenlet-3.2.4-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:671df96c1f23c4a0d4077a325483c1503c96a1b7d9db26592ae770daa41233d4"},
|
||||
{file = "greenlet-3.2.4-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:16458c245a38991aa19676900d48bd1a6f2ce3e16595051a4db9d012154e8433"},
|
||||
{file = "greenlet-3.2.4-cp39-cp39-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c9913f1a30e4526f432991f89ae263459b1c64d1608c0d22a5c79c287b3c70df"},
|
||||
{file = "greenlet-3.2.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:b90654e092f928f110e0007f572007c9727b5265f7632c2fa7415b4689351594"},
|
||||
{file = "greenlet-3.2.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:81701fd84f26330f0d5f4944d4e92e61afe6319dcd9775e39396e39d7c3e5f98"},
|
||||
{file = "greenlet-3.2.4-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:28a3c6b7cd72a96f61b0e4b2a36f681025b60ae4779cc73c1535eb5f29560b10"},
|
||||
{file = "greenlet-3.2.4-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:52206cd642670b0b320a1fd1cbfd95bca0e043179c1d8a045f2c6109dfe973be"},
|
||||
{file = "greenlet-3.2.4-cp39-cp39-win32.whl", hash = "sha256:65458b409c1ed459ea899e939f0e1cdb14f58dbc803f2f93c5eab5694d32671b"},
|
||||
{file = "greenlet-3.2.4-cp39-cp39-win_amd64.whl", hash = "sha256:d2e685ade4dafd447ede19c31277a224a239a0a1a4eca4e6390efedf20260cfb"},
|
||||
{file = "greenlet-3.2.4.tar.gz", hash = "sha256:0dca0d95ff849f9a364385f36ab49f50065d76964944638be9691e1832e9f86d"},
|
||||
]
|
||||
|
||||
[package.extras]
|
||||
docs = ["Sphinx", "furo"]
|
||||
test = ["objgraph", "psutil", "setuptools"]
|
||||
|
||||
[[package]]
|
||||
name = "gunicorn"
|
||||
version = "23.0.0"
|
||||
@@ -4223,29 +4046,6 @@ rsa = ["cryptography (>=3.0.0)"]
|
||||
signals = ["blinker (>=1.4.0)"]
|
||||
signedtoken = ["cryptography (>=3.0.0)", "pyjwt (>=2.0.0,<3)"]
|
||||
|
||||
[[package]]
|
||||
name = "oci"
|
||||
version = "2.160.3"
|
||||
description = "Oracle Cloud Infrastructure Python SDK"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "oci-2.160.3-py3-none-any.whl", hash = "sha256:858bff3e697098bdda44833d2476bfb4632126f0182178e7dbde4dbd156d71f0"},
|
||||
{file = "oci-2.160.3.tar.gz", hash = "sha256:57514889be3b713a8385d86e3ba8a33cf46e3563c2a7e29a93027fb30b8a2537"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
certifi = "*"
|
||||
circuitbreaker = {version = ">=1.3.1,<3.0.0", markers = "python_version >= \"3.7\""}
|
||||
cryptography = ">=3.2.1,<46.0.0"
|
||||
pyOpenSSL = ">=17.5.0,<25.0.0"
|
||||
python-dateutil = ">=2.5.3,<3.0.0"
|
||||
pytz = ">=2016.10"
|
||||
|
||||
[package.extras]
|
||||
adk = ["docstring-parser (>=0.16) ; python_version >= \"3.10\" and python_version < \"4\"", "mcp (>=1.6.0) ; python_version >= \"3.10\" and python_version < \"4\"", "pydantic (>=2.10.6) ; python_version >= \"3.10\" and python_version < \"4\"", "rich (>=13.9.4) ; python_version >= \"3.10\" and python_version < \"4\""]
|
||||
|
||||
[[package]]
|
||||
name = "openai"
|
||||
version = "1.101.0"
|
||||
@@ -4780,7 +4580,7 @@ files = [
|
||||
|
||||
[[package]]
|
||||
name = "prowler"
|
||||
version = "5.14.0"
|
||||
version = "5.13.0"
|
||||
description = "Prowler is an Open Source security tool to perform AWS, GCP and Azure security best practices assessments, audits, incident response, continuous monitoring, hardening and forensics readiness. It contains hundreds of controls covering CIS, NIST 800, NIST CSF, CISA, RBI, FedRAMP, PCI-DSS, GDPR, HIPAA, FFIEC, SOC2, GXP, AWS Well-Architected Framework Security Pillar, AWS Foundational Technical Review (FTR), ENS (Spanish National Security Scheme) and your custom security frameworks."
|
||||
optional = false
|
||||
python-versions = ">3.9.1,<3.13"
|
||||
@@ -4805,7 +4605,6 @@ azure-mgmt-keyvault = "10.3.1"
|
||||
azure-mgmt-loganalytics = "12.0.0"
|
||||
azure-mgmt-monitor = "6.0.2"
|
||||
azure-mgmt-network = "28.1.0"
|
||||
azure-mgmt-postgresqlflexibleservers = "1.1.0"
|
||||
azure-mgmt-rdbms = "10.1.0"
|
||||
azure-mgmt-recoveryservices = "3.1.0"
|
||||
azure-mgmt-recoveryservicesbackup = "9.2.0"
|
||||
@@ -4835,7 +4634,6 @@ markdown = "3.9.0"
|
||||
microsoft-kiota-abstractions = "1.9.2"
|
||||
msgraph-sdk = "1.23.0"
|
||||
numpy = "2.0.2"
|
||||
oci = "2.160.3"
|
||||
pandas = "2.2.3"
|
||||
py-iam-expand = "0.1.0"
|
||||
py-ocsf-models = "0.5.0"
|
||||
@@ -4852,8 +4650,8 @@ tzlocal = "5.3.1"
|
||||
[package.source]
|
||||
type = "git"
|
||||
url = "https://github.com/prowler-cloud/prowler.git"
|
||||
reference = "v5.14"
|
||||
resolved_reference = "3b05a1430e016cee92b60973705cba400255d9e5"
|
||||
reference = "master"
|
||||
resolved_reference = "a52697bfdfee83d14a49c11dcbe96888b5cd767e"
|
||||
|
||||
[[package]]
|
||||
name = "psutil"
|
||||
@@ -5338,25 +5136,6 @@ cffi = ">=1.4.1"
|
||||
docs = ["sphinx (>=1.6.5)", "sphinx-rtd-theme"]
|
||||
tests = ["hypothesis (>=3.27.0)", "pytest (>=3.2.1,!=3.3.0)"]
|
||||
|
||||
[[package]]
|
||||
name = "pyopenssl"
|
||||
version = "24.3.0"
|
||||
description = "Python wrapper module around the OpenSSL library"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "pyOpenSSL-24.3.0-py3-none-any.whl", hash = "sha256:e474f5a473cd7f92221cc04976e48f4d11502804657a08a989fb3be5514c904a"},
|
||||
{file = "pyopenssl-24.3.0.tar.gz", hash = "sha256:49f7a019577d834746bc55c5fce6ecbcec0f2b4ec5ce1cf43a9a173b8138bb36"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
cryptography = ">=41.0.5,<45"
|
||||
|
||||
[package.extras]
|
||||
docs = ["sphinx (!=5.2.0,!=5.2.0.post0,!=7.2.5)", "sphinx_rtd_theme"]
|
||||
test = ["pretend", "pytest (>=3.0.1)", "pytest-rerunfailures"]
|
||||
|
||||
[[package]]
|
||||
name = "pyparsing"
|
||||
version = "3.2.3"
|
||||
@@ -7004,69 +6783,7 @@ enabler = ["pytest-enabler (>=2.2)"]
|
||||
test = ["big-O", "jaraco.functools", "jaraco.itertools", "jaraco.test", "more_itertools", "pytest (>=6,!=8.1.*)", "pytest-ignore-flaky"]
|
||||
type = ["pytest-mypy"]
|
||||
|
||||
[[package]]
|
||||
name = "zope-event"
|
||||
version = "6.1"
|
||||
description = "Very basic event publishing system"
|
||||
optional = false
|
||||
python-versions = ">=3.10"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "zope_event-6.1-py3-none-any.whl", hash = "sha256:0ca78b6391b694272b23ec1335c0294cc471065ed10f7f606858fc54566c25a0"},
|
||||
{file = "zope_event-6.1.tar.gz", hash = "sha256:6052a3e0cb8565d3d4ef1a3a7809336ac519bc4fe38398cb8d466db09adef4f0"},
|
||||
]
|
||||
|
||||
[package.extras]
|
||||
docs = ["Sphinx"]
|
||||
test = ["zope.testrunner (>=6.4)"]
|
||||
|
||||
[[package]]
|
||||
name = "zope-interface"
|
||||
version = "8.1.1"
|
||||
description = "Interfaces for Python"
|
||||
optional = false
|
||||
python-versions = ">=3.10"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "zope_interface-8.1.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5c6b12b656c7d7e3d79cad8e2afc4a37eae6b6076e2c209a33345143148e435e"},
|
||||
{file = "zope_interface-8.1.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:557c0f1363c300db406e9eeaae8ab6d1ba429d4fed60d8ab7dadab5ca66ccd35"},
|
||||
{file = "zope_interface-8.1.1-cp310-cp310-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:127b0e4c873752b777721543cf8525b3db5e76b88bd33bab807f03c568e9003f"},
|
||||
{file = "zope_interface-8.1.1-cp310-cp310-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:e0892c9d2dd47b45f62d1861bcae8b427fcc49b4a04fff67f12c5c55e56654d7"},
|
||||
{file = "zope_interface-8.1.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ff8a92dc8c8a2c605074e464984e25b9b5a8ac9b2a0238dd73a0f374df59a77e"},
|
||||
{file = "zope_interface-8.1.1-cp310-cp310-win_amd64.whl", hash = "sha256:54627ddf6034aab1f506ba750dd093f67d353be6249467d720e9f278a578efe5"},
|
||||
{file = "zope_interface-8.1.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:e8a0fdd5048c1bb733e4693eae9bc4145a19419ea6a1c95299318a93fe9f3d72"},
|
||||
{file = "zope_interface-8.1.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a4cb0ea75a26b606f5bc8524fbce7b7d8628161b6da002c80e6417ce5ec757c0"},
|
||||
{file = "zope_interface-8.1.1-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:c267b00b5a49a12743f5e1d3b4beef45479d696dab090f11fe3faded078a5133"},
|
||||
{file = "zope_interface-8.1.1-cp311-cp311-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:e25d3e2b9299e7ec54b626573673bdf0d740cf628c22aef0a3afef85b438aa54"},
|
||||
{file = "zope_interface-8.1.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:63db1241804417aff95ac229c13376c8c12752b83cc06964d62581b493e6551b"},
|
||||
{file = "zope_interface-8.1.1-cp311-cp311-win_amd64.whl", hash = "sha256:9639bf4ed07b5277fb231e54109117c30d608254685e48a7104a34618bcbfc83"},
|
||||
{file = "zope_interface-8.1.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:a16715808408db7252b8c1597ed9008bdad7bf378ed48eb9b0595fad4170e49d"},
|
||||
{file = "zope_interface-8.1.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce6b58752acc3352c4aa0b55bbeae2a941d61537e6afdad2467a624219025aae"},
|
||||
{file = "zope_interface-8.1.1-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:807778883d07177713136479de7fd566f9056a13aef63b686f0ab4807c6be259"},
|
||||
{file = "zope_interface-8.1.1-cp312-cp312-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:50e5eb3b504a7d63dc25211b9298071d5b10a3eb754d6bf2f8ef06cb49f807ab"},
|
||||
{file = "zope_interface-8.1.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:eee6f93b2512ec9466cf30c37548fd3ed7bc4436ab29cd5943d7a0b561f14f0f"},
|
||||
{file = "zope_interface-8.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:80edee6116d569883c58ff8efcecac3b737733d646802036dc337aa839a5f06b"},
|
||||
{file = "zope_interface-8.1.1-cp313-cp313-macosx_10_9_x86_64.whl", hash = "sha256:84f9be6d959640de9da5d14ac1f6a89148b16da766e88db37ed17e936160b0b1"},
|
||||
{file = "zope_interface-8.1.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:531fba91dcb97538f70cf4642a19d6574269460274e3f6004bba6fe684449c51"},
|
||||
{file = "zope_interface-8.1.1-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:fc65f5633d5a9583ee8d88d1f5de6b46cd42c62e47757cfe86be36fb7c8c4c9b"},
|
||||
{file = "zope_interface-8.1.1-cp313-cp313-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:efef80ddec4d7d99618ef71bc93b88859248075ca2e1ae1c78636654d3d55533"},
|
||||
{file = "zope_interface-8.1.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:49aad83525eca3b4747ef51117d302e891f0042b06f32aa1c7023c62642f962b"},
|
||||
{file = "zope_interface-8.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:71cf329a21f98cb2bd9077340a589e316ac8a415cac900575a32544b3dffcb98"},
|
||||
{file = "zope_interface-8.1.1-cp314-cp314-macosx_10_9_x86_64.whl", hash = "sha256:da311e9d253991ca327601f47c4644d72359bac6950fbb22f971b24cd7850f8c"},
|
||||
{file = "zope_interface-8.1.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:3fb25fca0442c7fb93c4ee40b42e3e033fef2f648730c4b7ae6d43222a3e8946"},
|
||||
{file = "zope_interface-8.1.1-cp314-cp314-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:bac588d0742b4e35efb7c7df1dacc0397b51ed37a17d4169a38019a1cebacf0a"},
|
||||
{file = "zope_interface-8.1.1-cp314-cp314-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:3d1f053d2d5e2b393e619bce1e55954885c2e63969159aa521839e719442db49"},
|
||||
{file = "zope_interface-8.1.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:64a1ad7f4cb17d948c6bdc525a1d60c0e567b2526feb4fa38b38f249961306b8"},
|
||||
{file = "zope_interface-8.1.1-cp314-cp314-win_amd64.whl", hash = "sha256:169214da1b82b7695d1a36f92d70b11166d66b6b09d03df35d150cc62ac52276"},
|
||||
{file = "zope_interface-8.1.1.tar.gz", hash = "sha256:51b10e6e8e238d719636a401f44f1e366146912407b58453936b781a19be19ec"},
|
||||
]
|
||||
|
||||
[package.extras]
|
||||
docs = ["Sphinx", "furo", "repoze.sphinx.autointerface"]
|
||||
test = ["coverage[toml]", "zope.event", "zope.testing"]
|
||||
testing = ["coverage[toml]", "zope.event", "zope.testing"]
|
||||
|
||||
[metadata]
|
||||
lock-version = "2.1"
|
||||
python-versions = ">=3.11,<3.13"
|
||||
content-hash = "6dcdbbed2a46ab0111f4e32979fb7e5c7e3f6a80c4d293ac21b8c1f73c555204"
|
||||
content-hash = "943e2cd6b87229704550d4e140b36509fb9f58896ebb5834b9fbabe28a9ee92f"
|
||||
|
||||
@@ -24,7 +24,7 @@ dependencies = [
|
||||
"drf-spectacular-jsonapi==0.5.1",
|
||||
"gunicorn==23.0.0",
|
||||
"lxml==5.3.2",
|
||||
"prowler @ git+https://github.com/prowler-cloud/prowler.git@v5.14",
|
||||
"prowler @ git+https://github.com/prowler-cloud/prowler.git@master",
|
||||
"psycopg2-binary==2.9.9",
|
||||
"pytest-celery[redis] (>=1.0.1,<2.0.0)",
|
||||
"sentry-sdk[django] (>=2.20.0,<3.0.0)",
|
||||
@@ -35,8 +35,7 @@ dependencies = [
|
||||
"markdown (>=3.9,<4.0)",
|
||||
"drf-simple-apikey (==2.2.1)",
|
||||
"matplotlib (>=3.10.6,<4.0.0)",
|
||||
"reportlab (>=4.4.4,<5.0.0)",
|
||||
"gevent (>=25.9.1,<26.0.0)"
|
||||
"reportlab (>=4.4.4,<5.0.0)"
|
||||
]
|
||||
description = "Prowler's API (Django/DRF)"
|
||||
license = "Apache-2.0"
|
||||
@@ -44,7 +43,7 @@ name = "prowler-api"
|
||||
package-mode = false
|
||||
# Needed for the SDK compatibility
|
||||
requires-python = ">=3.11,<3.13"
|
||||
version = "1.15.2"
|
||||
version = "1.15.0"
|
||||
|
||||
[project.scripts]
|
||||
celery = "src.backend.config.settings.celery"
|
||||
|
||||
@@ -761,14 +761,6 @@ 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:
|
||||
@@ -820,8 +812,7 @@ class ScanSummarySeverityFilter(ScanSummaryFilter):
|
||||
elif value == OverviewStatusChoices.PASS:
|
||||
return queryset.annotate(status_count=F("_pass"))
|
||||
else:
|
||||
# Exclude muted findings by default
|
||||
return queryset.annotate(status_count=F("_pass") + F("fail"))
|
||||
return queryset.annotate(status_count=F("total"))
|
||||
|
||||
def filter_status_in(self, queryset, name, value):
|
||||
# Validate the status values
|
||||
@@ -830,7 +821,7 @@ class ScanSummarySeverityFilter(ScanSummaryFilter):
|
||||
if status_val not in valid_statuses:
|
||||
raise ValidationError(f"Invalid status value: {status_val}")
|
||||
|
||||
# If all statuses or no valid statuses, exclude muted findings (pass + fail)
|
||||
# If all statuses or no valid statuses, use total
|
||||
if (
|
||||
set(value)
|
||||
>= {
|
||||
@@ -839,7 +830,7 @@ class ScanSummarySeverityFilter(ScanSummaryFilter):
|
||||
}
|
||||
or not value
|
||||
):
|
||||
return queryset.annotate(status_count=F("_pass") + F("fail"))
|
||||
return queryset.annotate(status_count=F("total"))
|
||||
|
||||
# Build the sum expression based on status values
|
||||
sum_expression = None
|
||||
@@ -857,7 +848,7 @@ class ScanSummarySeverityFilter(ScanSummaryFilter):
|
||||
sum_expression = sum_expression + field_expr
|
||||
|
||||
if sum_expression is None:
|
||||
return queryset.annotate(status_count=F("_pass") + F("fail"))
|
||||
return queryset.annotate(status_count=F("total"))
|
||||
|
||||
return queryset.annotate(status_count=sum_expression)
|
||||
|
||||
@@ -869,6 +860,26 @@ 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)
|
||||
|
||||
@@ -22,7 +22,7 @@ class Migration(migrations.Migration):
|
||||
("kubernetes", "Kubernetes"),
|
||||
("m365", "M365"),
|
||||
("github", "GitHub"),
|
||||
("oraclecloud", "Oracle Cloud Infrastructure"),
|
||||
("oci", "Oracle Cloud Infrastructure"),
|
||||
("iac", "IaC"),
|
||||
],
|
||||
default="aws",
|
||||
|
||||
@@ -29,8 +29,4 @@ class Migration(migrations.Migration):
|
||||
default="aws",
|
||||
),
|
||||
),
|
||||
migrations.RunSQL(
|
||||
"ALTER TYPE provider ADD VALUE IF NOT EXISTS 'mongodbatlas';",
|
||||
reverse_sql=migrations.RunSQL.noop,
|
||||
),
|
||||
]
|
||||
|
||||
@@ -22,8 +22,4 @@ class Migration(migrations.Migration):
|
||||
model_name="compliancerequirementoverview",
|
||||
name="cro_scan_comp_req_idx",
|
||||
),
|
||||
RemoveIndexConcurrently(
|
||||
model_name="compliancerequirementoverview",
|
||||
name="cro_scan_comp_req_reg_idx",
|
||||
),
|
||||
]
|
||||
|
||||
@@ -2282,6 +2282,9 @@ class ThreatScoreSnapshot(RowLevelSecurityProtectedModel):
|
||||
Snapshots are created automatically after each ThreatScore report generation.
|
||||
"""
|
||||
|
||||
objects = models.Manager()
|
||||
all_objects = models.Manager()
|
||||
|
||||
id = models.UUIDField(primary_key=True, default=uuid4, editable=False)
|
||||
inserted_at = models.DateTimeField(auto_now_add=True, editable=False)
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
openapi: 3.0.3
|
||||
info:
|
||||
title: Prowler API
|
||||
version: 1.15.2
|
||||
version: 1.15.0
|
||||
description: |-
|
||||
Prowler API specification.
|
||||
|
||||
@@ -283,11 +283,8 @@ paths:
|
||||
/api/v1/compliance-overviews:
|
||||
get:
|
||||
operationId: compliance_overviews_list
|
||||
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.
|
||||
summary: List compliance overviews
|
||||
description: Retrieve an overview of all the compliance in a given scan.
|
||||
summary: List compliance overviews for a scan
|
||||
parameters:
|
||||
- in: query
|
||||
name: fields[compliance-overviews]
|
||||
@@ -346,32 +343,6 @@ paths:
|
||||
schema:
|
||||
type: string
|
||||
format: date-time
|
||||
- in: query
|
||||
name: filter[provider_id]
|
||||
schema:
|
||||
type: string
|
||||
format: uuid
|
||||
description: Filter by specific provider ID.
|
||||
- in: query
|
||||
name: filter[provider_id__in]
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
format: uuid
|
||||
description: Filter by multiple provider IDs (comma-separated).
|
||||
- in: query
|
||||
name: filter[provider_type]
|
||||
schema:
|
||||
type: string
|
||||
description: Filter by provider type (e.g., aws, azure, gcp).
|
||||
- in: query
|
||||
name: filter[provider_type__in]
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
description: Filter by multiple provider types (comma-separated).
|
||||
- in: query
|
||||
name: filter[region]
|
||||
schema:
|
||||
@@ -394,8 +365,8 @@ paths:
|
||||
schema:
|
||||
type: string
|
||||
format: uuid
|
||||
description: Optional scan ID. If provided, returns compliance for that scan.
|
||||
If omitted, returns compliance for the latest completed scan per provider.
|
||||
description: Related scan ID.
|
||||
required: true
|
||||
- name: filter[search]
|
||||
required: false
|
||||
in: query
|
||||
@@ -635,77 +606,6 @@ paths:
|
||||
schema:
|
||||
type: string
|
||||
format: date-time
|
||||
- in: query
|
||||
name: filter[provider_id]
|
||||
schema:
|
||||
type: string
|
||||
format: uuid
|
||||
- in: query
|
||||
name: filter[provider_id__in]
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
format: uuid
|
||||
description: Multiple values may be separated by commas.
|
||||
explode: false
|
||||
style: form
|
||||
- in: query
|
||||
name: filter[provider_type]
|
||||
schema:
|
||||
type: string
|
||||
x-spec-enum-id: eca8c51e6bd28935
|
||||
enum:
|
||||
- aws
|
||||
- azure
|
||||
- gcp
|
||||
- github
|
||||
- iac
|
||||
- kubernetes
|
||||
- m365
|
||||
- mongodbatlas
|
||||
- oraclecloud
|
||||
description: |-
|
||||
* `aws` - AWS
|
||||
* `azure` - Azure
|
||||
* `gcp` - GCP
|
||||
* `kubernetes` - Kubernetes
|
||||
* `m365` - M365
|
||||
* `github` - GitHub
|
||||
* `mongodbatlas` - MongoDB Atlas
|
||||
* `iac` - IaC
|
||||
* `oraclecloud` - Oracle Cloud Infrastructure
|
||||
- in: query
|
||||
name: filter[provider_type__in]
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
x-spec-enum-id: eca8c51e6bd28935
|
||||
enum:
|
||||
- aws
|
||||
- azure
|
||||
- gcp
|
||||
- github
|
||||
- iac
|
||||
- kubernetes
|
||||
- m365
|
||||
- mongodbatlas
|
||||
- oraclecloud
|
||||
description: |-
|
||||
Multiple values may be separated by commas.
|
||||
|
||||
* `aws` - AWS
|
||||
* `azure` - Azure
|
||||
* `gcp` - GCP
|
||||
* `kubernetes` - Kubernetes
|
||||
* `m365` - M365
|
||||
* `github` - GitHub
|
||||
* `mongodbatlas` - MongoDB Atlas
|
||||
* `iac` - IaC
|
||||
* `oraclecloud` - Oracle Cloud Infrastructure
|
||||
explode: false
|
||||
style: form
|
||||
- in: query
|
||||
name: filter[region]
|
||||
schema:
|
||||
@@ -5051,181 +4951,13 @@ paths:
|
||||
schema:
|
||||
$ref: '#/components/schemas/OverviewProviderCountResponse'
|
||||
description: ''
|
||||
/api/v1/overviews/regions:
|
||||
get:
|
||||
operationId: overviews_regions_retrieve
|
||||
description: Retrieve an aggregated summary of findings grouped by region. The
|
||||
response includes the total, passed, failed, and muted findings for each region
|
||||
based on the latest completed scans per provider. Standard overview filters
|
||||
(inserted_at, provider filters, region filters, etc.) are supported.
|
||||
summary: Get findings data by region
|
||||
parameters:
|
||||
- in: query
|
||||
name: fields[regions-overview]
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
enum:
|
||||
- id
|
||||
- total
|
||||
- fail
|
||||
- muted
|
||||
- pass
|
||||
description: endpoint return only specific fields in the response on a per-type
|
||||
basis by including a fields[TYPE] query parameter.
|
||||
explode: false
|
||||
- in: query
|
||||
name: filter[inserted_at]
|
||||
schema:
|
||||
type: string
|
||||
format: date
|
||||
- in: query
|
||||
name: filter[inserted_at__date]
|
||||
schema:
|
||||
type: string
|
||||
format: date
|
||||
- in: query
|
||||
name: filter[inserted_at__gte]
|
||||
schema:
|
||||
type: string
|
||||
format: date-time
|
||||
- in: query
|
||||
name: filter[inserted_at__lte]
|
||||
schema:
|
||||
type: string
|
||||
format: date-time
|
||||
- in: query
|
||||
name: filter[provider_id]
|
||||
schema:
|
||||
type: string
|
||||
format: uuid
|
||||
- in: query
|
||||
name: filter[provider_id__in]
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
format: uuid
|
||||
description: Multiple values may be separated by commas.
|
||||
explode: false
|
||||
style: form
|
||||
- in: query
|
||||
name: filter[provider_type]
|
||||
schema:
|
||||
type: string
|
||||
x-spec-enum-id: eca8c51e6bd28935
|
||||
enum:
|
||||
- aws
|
||||
- azure
|
||||
- gcp
|
||||
- github
|
||||
- iac
|
||||
- kubernetes
|
||||
- m365
|
||||
- mongodbatlas
|
||||
- oraclecloud
|
||||
description: |-
|
||||
* `aws` - AWS
|
||||
* `azure` - Azure
|
||||
* `gcp` - GCP
|
||||
* `kubernetes` - Kubernetes
|
||||
* `m365` - M365
|
||||
* `github` - GitHub
|
||||
* `mongodbatlas` - MongoDB Atlas
|
||||
* `iac` - IaC
|
||||
* `oraclecloud` - Oracle Cloud Infrastructure
|
||||
- in: query
|
||||
name: filter[provider_type__in]
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
x-spec-enum-id: eca8c51e6bd28935
|
||||
enum:
|
||||
- aws
|
||||
- azure
|
||||
- gcp
|
||||
- github
|
||||
- iac
|
||||
- kubernetes
|
||||
- m365
|
||||
- mongodbatlas
|
||||
- oraclecloud
|
||||
description: |-
|
||||
Multiple values may be separated by commas.
|
||||
|
||||
* `aws` - AWS
|
||||
* `azure` - Azure
|
||||
* `gcp` - GCP
|
||||
* `kubernetes` - Kubernetes
|
||||
* `m365` - M365
|
||||
* `github` - GitHub
|
||||
* `mongodbatlas` - MongoDB Atlas
|
||||
* `iac` - IaC
|
||||
* `oraclecloud` - Oracle Cloud Infrastructure
|
||||
explode: false
|
||||
style: form
|
||||
- in: query
|
||||
name: filter[region]
|
||||
schema:
|
||||
type: string
|
||||
- in: query
|
||||
name: filter[region__icontains]
|
||||
schema:
|
||||
type: string
|
||||
- in: query
|
||||
name: filter[region__in]
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
description: Multiple values may be separated by commas.
|
||||
explode: false
|
||||
style: form
|
||||
- name: filter[search]
|
||||
required: false
|
||||
in: query
|
||||
description: A search term.
|
||||
schema:
|
||||
type: string
|
||||
- name: sort
|
||||
required: false
|
||||
in: query
|
||||
description: '[list of fields to sort by](https://jsonapi.org/format/#fetching-sorting)'
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
enum:
|
||||
- id
|
||||
- -id
|
||||
- total
|
||||
- -total
|
||||
- fail
|
||||
- -fail
|
||||
- muted
|
||||
- -muted
|
||||
- pass
|
||||
- -pass
|
||||
explode: false
|
||||
tags:
|
||||
- Overview
|
||||
security:
|
||||
- JWT or API Key: []
|
||||
responses:
|
||||
'200':
|
||||
content:
|
||||
application/vnd.api+json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/OverviewRegionResponse'
|
||||
description: ''
|
||||
/api/v1/overviews/services:
|
||||
get:
|
||||
operationId: overviews_services_retrieve
|
||||
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.
|
||||
as there are at least one finding for that service. At least one of the `inserted_at`
|
||||
filters must be provided.
|
||||
summary: Get findings data by service
|
||||
parameters:
|
||||
- in: query
|
||||
@@ -5388,90 +5120,6 @@ paths:
|
||||
schema:
|
||||
$ref: '#/components/schemas/OverviewServiceResponse'
|
||||
description: ''
|
||||
/api/v1/overviews/threatscore:
|
||||
get:
|
||||
operationId: overviews_threatscore_retrieve
|
||||
description: Retrieve ThreatScore metrics. By default, returns the latest snapshot
|
||||
for each provider. Use snapshot_id to retrieve a specific historical snapshot.
|
||||
summary: Get ThreatScore snapshots
|
||||
parameters:
|
||||
- in: query
|
||||
name: fields[threatscore-snapshots]
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
enum:
|
||||
- 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
|
||||
description: endpoint return only specific fields in the response on a per-type
|
||||
basis by including a fields[TYPE] query parameter.
|
||||
explode: false
|
||||
- in: query
|
||||
name: include
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
enum:
|
||||
- scan
|
||||
- provider
|
||||
description: include query parameter to allow the client to customize which
|
||||
related resources should be returned.
|
||||
explode: false
|
||||
- in: query
|
||||
name: provider_id
|
||||
schema:
|
||||
type: string
|
||||
format: uuid
|
||||
description: Filter by specific provider ID
|
||||
- in: query
|
||||
name: provider_id__in
|
||||
schema:
|
||||
type: string
|
||||
description: Filter by multiple provider IDs (comma-separated UUIDs)
|
||||
- in: query
|
||||
name: provider_type
|
||||
schema:
|
||||
type: string
|
||||
description: Filter by provider type (aws, azure, gcp, etc.)
|
||||
- in: query
|
||||
name: provider_type__in
|
||||
schema:
|
||||
type: string
|
||||
description: Filter by multiple provider types (comma-separated)
|
||||
- in: query
|
||||
name: snapshot_id
|
||||
schema:
|
||||
type: string
|
||||
format: uuid
|
||||
description: Retrieve a specific snapshot by ID. If not provided, returns
|
||||
latest snapshots.
|
||||
tags:
|
||||
- Overview
|
||||
security:
|
||||
- JWT or API Key: []
|
||||
responses:
|
||||
'200':
|
||||
content:
|
||||
application/vnd.api+json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ThreatScoreSnapshotResponse'
|
||||
description: ''
|
||||
/api/v1/processors:
|
||||
get:
|
||||
operationId: processors_list
|
||||
@@ -9088,138 +8736,6 @@ paths:
|
||||
'404':
|
||||
description: The scan has no threatscore reports, or the threatscore report
|
||||
generation task has not started yet
|
||||
/api/v1/scans/{id}/ens:
|
||||
get:
|
||||
operationId: scans_ens_retrieve
|
||||
description: Download a specific ENS compliance report (e.g., 'prowler_ens_aws')
|
||||
as a PDF file.
|
||||
summary: Retrieve ENS compliance report
|
||||
parameters:
|
||||
- in: query
|
||||
name: fields[scans]
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
enum:
|
||||
- name
|
||||
- trigger
|
||||
- state
|
||||
- unique_resource_count
|
||||
- progress
|
||||
- duration
|
||||
- provider
|
||||
- task
|
||||
- inserted_at
|
||||
- started_at
|
||||
- completed_at
|
||||
- scheduled_at
|
||||
- next_scan_at
|
||||
- processor
|
||||
- url
|
||||
description: endpoint return only specific fields in the response on a per-type
|
||||
basis by including a fields[TYPE] query parameter.
|
||||
explode: false
|
||||
- in: path
|
||||
name: id
|
||||
schema:
|
||||
type: string
|
||||
format: uuid
|
||||
description: A UUID string identifying this scan.
|
||||
required: true
|
||||
- in: query
|
||||
name: include
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
enum:
|
||||
- provider
|
||||
description: include query parameter to allow the client to customize which
|
||||
related resources should be returned.
|
||||
explode: false
|
||||
tags:
|
||||
- Scan
|
||||
security:
|
||||
- JWT or API Key: []
|
||||
responses:
|
||||
'200':
|
||||
description: PDF file containing the ENS compliance report
|
||||
'202':
|
||||
description: The task is in progress
|
||||
'401':
|
||||
description: API key missing or user not Authenticated
|
||||
'403':
|
||||
description: There is a problem with credentials
|
||||
'404':
|
||||
description: The scan has no ENS reports, or the ENS report generation task
|
||||
has not started yet
|
||||
/api/v1/scans/{id}/nis2:
|
||||
get:
|
||||
operationId: scans_nis2_retrieve
|
||||
description: Download NIS2 compliance report (Directive (EU) 2022/2555) as a
|
||||
PDF file.
|
||||
summary: Retrieve NIS2 compliance report
|
||||
parameters:
|
||||
- in: query
|
||||
name: fields[scans]
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
enum:
|
||||
- name
|
||||
- trigger
|
||||
- state
|
||||
- unique_resource_count
|
||||
- progress
|
||||
- duration
|
||||
- provider
|
||||
- task
|
||||
- inserted_at
|
||||
- started_at
|
||||
- completed_at
|
||||
- scheduled_at
|
||||
- next_scan_at
|
||||
- processor
|
||||
- url
|
||||
description: endpoint return only specific fields in the response on a per-type
|
||||
basis by including a fields[TYPE] query parameter.
|
||||
explode: false
|
||||
- in: path
|
||||
name: id
|
||||
schema:
|
||||
type: string
|
||||
format: uuid
|
||||
description: A UUID string identifying this scan.
|
||||
required: true
|
||||
- in: query
|
||||
name: include
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
enum:
|
||||
- provider
|
||||
description: include query parameter to allow the client to customize which
|
||||
related resources should be returned.
|
||||
explode: false
|
||||
tags:
|
||||
- Scan
|
||||
security:
|
||||
- JWT or API Key: []
|
||||
responses:
|
||||
'200':
|
||||
description: PDF file containing the NIS2 compliance report
|
||||
'202':
|
||||
description: The task is in progress
|
||||
'401':
|
||||
description: API key missing or user not Authenticated
|
||||
'403':
|
||||
description: There is a problem with credentials
|
||||
'404':
|
||||
description: The scan has no NIS2 reports, or the NIS2 report generation
|
||||
task has not started yet
|
||||
/api/v1/schedules/daily:
|
||||
post:
|
||||
operationId: schedules_daily_create
|
||||
@@ -13538,47 +13054,6 @@ components:
|
||||
$ref: '#/components/schemas/OverviewProvider'
|
||||
required:
|
||||
- data
|
||||
OverviewRegion:
|
||||
type: object
|
||||
required:
|
||||
- type
|
||||
- id
|
||||
additionalProperties: false
|
||||
properties:
|
||||
type:
|
||||
type: string
|
||||
description: The [type](https://jsonapi.org/format/#document-resource-object-identification)
|
||||
member is used to describe resource objects that share common attributes
|
||||
and relationships.
|
||||
enum:
|
||||
- regions-overview
|
||||
id: {}
|
||||
attributes:
|
||||
type: object
|
||||
properties:
|
||||
id:
|
||||
type: string
|
||||
total:
|
||||
type: integer
|
||||
fail:
|
||||
type: integer
|
||||
muted:
|
||||
type: integer
|
||||
pass:
|
||||
type: integer
|
||||
required:
|
||||
- id
|
||||
- total
|
||||
- fail
|
||||
- muted
|
||||
- pass
|
||||
OverviewRegionResponse:
|
||||
type: object
|
||||
properties:
|
||||
data:
|
||||
$ref: '#/components/schemas/OverviewRegion'
|
||||
required:
|
||||
- data
|
||||
OverviewService:
|
||||
type: object
|
||||
required:
|
||||
@@ -19106,143 +18581,6 @@ components:
|
||||
$ref: '#/components/schemas/Tenant'
|
||||
required:
|
||||
- data
|
||||
ThreatScoreSnapshot:
|
||||
type: object
|
||||
required:
|
||||
- type
|
||||
- id
|
||||
additionalProperties: false
|
||||
properties:
|
||||
type:
|
||||
type: string
|
||||
description: The [type](https://jsonapi.org/format/#document-resource-object-identification)
|
||||
member is used to describe resource objects that share common attributes
|
||||
and relationships.
|
||||
enum:
|
||||
- threatscore-snapshots
|
||||
id: {}
|
||||
attributes:
|
||||
type: object
|
||||
properties:
|
||||
id:
|
||||
type: string
|
||||
readOnly: true
|
||||
inserted_at:
|
||||
type: string
|
||||
format: date-time
|
||||
readOnly: true
|
||||
compliance_id:
|
||||
type: string
|
||||
readOnly: true
|
||||
description: Compliance framework ID (e.g., 'prowler_threatscore_aws')
|
||||
overall_score:
|
||||
type: string
|
||||
format: decimal
|
||||
pattern: ^-?\d{0,3}(?:\.\d{0,2})?$
|
||||
readOnly: true
|
||||
description: Overall ThreatScore percentage (0-100)
|
||||
score_delta:
|
||||
type: string
|
||||
format: decimal
|
||||
pattern: ^-?\d{0,3}(?:\.\d{0,2})?$
|
||||
readOnly: true
|
||||
nullable: true
|
||||
description: Score change compared to previous snapshot (positive =
|
||||
improvement)
|
||||
section_scores:
|
||||
readOnly: true
|
||||
description: ThreatScore breakdown by section
|
||||
critical_requirements:
|
||||
readOnly: true
|
||||
description: List of critical failed requirements (risk >= 4)
|
||||
total_requirements:
|
||||
type: integer
|
||||
readOnly: true
|
||||
description: Total number of requirements evaluated
|
||||
passed_requirements:
|
||||
type: integer
|
||||
readOnly: true
|
||||
description: Number of requirements with PASS status
|
||||
failed_requirements:
|
||||
type: integer
|
||||
readOnly: true
|
||||
description: Number of requirements with FAIL status
|
||||
manual_requirements:
|
||||
type: integer
|
||||
readOnly: true
|
||||
description: Number of requirements with MANUAL status
|
||||
total_findings:
|
||||
type: integer
|
||||
readOnly: true
|
||||
description: Total number of findings across all requirements
|
||||
passed_findings:
|
||||
type: integer
|
||||
readOnly: true
|
||||
description: Number of findings with PASS status
|
||||
failed_findings:
|
||||
type: integer
|
||||
readOnly: true
|
||||
description: Number of findings with FAIL status
|
||||
relationships:
|
||||
type: object
|
||||
properties:
|
||||
scan:
|
||||
type: object
|
||||
properties:
|
||||
data:
|
||||
type: object
|
||||
properties:
|
||||
id:
|
||||
type: string
|
||||
format: uuid
|
||||
type:
|
||||
type: string
|
||||
enum:
|
||||
- scans
|
||||
title: Resource Type Name
|
||||
description: The [type](https://jsonapi.org/format/#document-resource-object-identification)
|
||||
member is used to describe resource objects that share common
|
||||
attributes and relationships.
|
||||
required:
|
||||
- id
|
||||
- type
|
||||
required:
|
||||
- data
|
||||
description: The identifier of the related object.
|
||||
title: Resource Identifier
|
||||
readOnly: true
|
||||
provider:
|
||||
type: object
|
||||
properties:
|
||||
data:
|
||||
type: object
|
||||
properties:
|
||||
id:
|
||||
type: string
|
||||
format: uuid
|
||||
type:
|
||||
type: string
|
||||
enum:
|
||||
- providers
|
||||
title: Resource Type Name
|
||||
description: The [type](https://jsonapi.org/format/#document-resource-object-identification)
|
||||
member is used to describe resource objects that share common
|
||||
attributes and relationships.
|
||||
required:
|
||||
- id
|
||||
- type
|
||||
required:
|
||||
- data
|
||||
description: The identifier of the related object.
|
||||
title: Resource Identifier
|
||||
readOnly: true
|
||||
ThreatScoreSnapshotResponse:
|
||||
type: object
|
||||
properties:
|
||||
data:
|
||||
$ref: '#/components/schemas/ThreatScoreSnapshot'
|
||||
required:
|
||||
- data
|
||||
Token:
|
||||
type: object
|
||||
required:
|
||||
|
||||
@@ -21,7 +21,6 @@ from prowler.providers.aws.lib.security_hub.security_hub import SecurityHubConne
|
||||
from prowler.providers.azure.azure_provider import AzureProvider
|
||||
from prowler.providers.gcp.gcp_provider import GcpProvider
|
||||
from prowler.providers.github.github_provider import GithubProvider
|
||||
from prowler.providers.iac.iac_provider import IacProvider
|
||||
from prowler.providers.kubernetes.kubernetes_provider import KubernetesProvider
|
||||
from prowler.providers.m365.m365_provider import M365Provider
|
||||
from prowler.providers.mongodbatlas.mongodbatlas_provider import MongodbatlasProvider
|
||||
@@ -115,7 +114,6 @@ class TestReturnProwlerProvider:
|
||||
(Provider.ProviderChoices.GITHUB.value, GithubProvider),
|
||||
(Provider.ProviderChoices.MONGODBATLAS.value, MongodbatlasProvider),
|
||||
(Provider.ProviderChoices.ORACLECLOUD.value, OraclecloudProvider),
|
||||
(Provider.ProviderChoices.IAC.value, IacProvider),
|
||||
],
|
||||
)
|
||||
def test_return_prowler_provider(self, provider_type, expected_provider):
|
||||
@@ -256,72 +254,6 @@ class TestGetProwlerProviderKwargs:
|
||||
expected_result = {**secret_dict, "mutelist_content": {"key": "value"}}
|
||||
assert result == expected_result
|
||||
|
||||
def test_get_prowler_provider_kwargs_iac_provider(self):
|
||||
"""Test that IaC provider gets correct kwargs with repository URL."""
|
||||
provider_uid = "https://github.com/org/repo"
|
||||
secret_dict = {"access_token": "test_token"}
|
||||
secret_mock = MagicMock()
|
||||
secret_mock.secret = secret_dict
|
||||
|
||||
provider = MagicMock()
|
||||
provider.provider = Provider.ProviderChoices.IAC.value
|
||||
provider.secret = secret_mock
|
||||
provider.uid = provider_uid
|
||||
|
||||
result = get_prowler_provider_kwargs(provider)
|
||||
|
||||
expected_result = {
|
||||
"scan_repository_url": provider_uid,
|
||||
"oauth_app_token": "test_token",
|
||||
}
|
||||
assert result == expected_result
|
||||
|
||||
def test_get_prowler_provider_kwargs_iac_provider_without_token(self):
|
||||
"""Test that IaC provider works without access token for public repos."""
|
||||
provider_uid = "https://github.com/org/public-repo"
|
||||
secret_dict = {}
|
||||
secret_mock = MagicMock()
|
||||
secret_mock.secret = secret_dict
|
||||
|
||||
provider = MagicMock()
|
||||
provider.provider = Provider.ProviderChoices.IAC.value
|
||||
provider.secret = secret_mock
|
||||
provider.uid = provider_uid
|
||||
|
||||
result = get_prowler_provider_kwargs(provider)
|
||||
|
||||
expected_result = {"scan_repository_url": provider_uid}
|
||||
assert result == expected_result
|
||||
|
||||
def test_get_prowler_provider_kwargs_iac_provider_ignores_mutelist(self):
|
||||
"""Test that IaC provider does NOT receive mutelist_content.
|
||||
|
||||
IaC provider uses Trivy's built-in mutelist logic, so it should not
|
||||
receive mutelist_content even when a mutelist processor is configured.
|
||||
"""
|
||||
provider_uid = "https://github.com/org/repo"
|
||||
secret_dict = {"access_token": "test_token"}
|
||||
secret_mock = MagicMock()
|
||||
secret_mock.secret = secret_dict
|
||||
|
||||
mutelist_processor = MagicMock()
|
||||
mutelist_processor.configuration = {"Mutelist": {"key": "value"}}
|
||||
|
||||
provider = MagicMock()
|
||||
provider.provider = Provider.ProviderChoices.IAC.value
|
||||
provider.secret = secret_mock
|
||||
provider.uid = provider_uid
|
||||
|
||||
result = get_prowler_provider_kwargs(provider, mutelist_processor)
|
||||
|
||||
# IaC provider should NOT have mutelist_content
|
||||
assert "mutelist_content" not in result
|
||||
expected_result = {
|
||||
"scan_repository_url": provider_uid,
|
||||
"oauth_app_token": "test_token",
|
||||
}
|
||||
assert result == expected_result
|
||||
|
||||
def test_get_prowler_provider_kwargs_unsupported_provider(self):
|
||||
# Setup
|
||||
provider_uid = "provider_uid"
|
||||
|
||||
@@ -5972,28 +5972,6 @@ class TestComplianceOverviewViewSet:
|
||||
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
|
||||
):
|
||||
@@ -6280,10 +6258,10 @@ class TestOverviewViewSet:
|
||||
response = authenticated_client.get(reverse("overview-providers"))
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
assert len(response.json()["data"]) == 1
|
||||
assert response.json()["data"][0]["attributes"]["findings"]["total"] == 9
|
||||
assert response.json()["data"][0]["attributes"]["findings"]["total"] == 4
|
||||
assert response.json()["data"][0]["attributes"]["findings"]["pass"] == 2
|
||||
assert response.json()["data"][0]["attributes"]["findings"]["fail"] == 1
|
||||
assert response.json()["data"][0]["attributes"]["findings"]["muted"] == 6
|
||||
assert response.json()["data"][0]["attributes"]["findings"]["muted"] == 1
|
||||
# Aggregated resources include all AWS providers present in the tenant
|
||||
assert response.json()["data"][0]["attributes"]["resources"]["total"] == 3
|
||||
|
||||
@@ -6335,10 +6313,10 @@ class TestOverviewViewSet:
|
||||
assert len(data) == 1
|
||||
attributes = data[0]["attributes"]
|
||||
|
||||
assert attributes["findings"]["total"] == 15
|
||||
assert attributes["findings"]["total"] == 10
|
||||
assert attributes["findings"]["pass"] == 5
|
||||
assert attributes["findings"]["fail"] == 3
|
||||
assert attributes["findings"]["muted"] == 7
|
||||
assert attributes["findings"]["muted"] == 2
|
||||
assert attributes["resources"]["total"] == 4
|
||||
|
||||
def test_overview_providers_count(
|
||||
@@ -6780,35 +6758,7 @@ class TestOverviewViewSet:
|
||||
self, authenticated_client, scan_summaries_fixture
|
||||
):
|
||||
response = authenticated_client.get(reverse("overview-services"))
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
# Should return services from latest scans
|
||||
assert len(response.json()["data"]) == 2
|
||||
|
||||
def test_overview_regions_list(self, authenticated_client, scan_summaries_fixture):
|
||||
response = authenticated_client.get(
|
||||
reverse("overview-regions"), {"filter[inserted_at]": TODAY}
|
||||
)
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
# Only two different regions in the fixture (region1, region2)
|
||||
assert len(response.json()["data"]) == 2
|
||||
|
||||
data = response.json()["data"]
|
||||
regions = {item["id"]: item["attributes"] for item in data}
|
||||
|
||||
assert "aws:region1" in regions
|
||||
assert "aws:region2" in regions
|
||||
|
||||
# region1 has 5 findings (2 pass, 0 fail, 3 muted)
|
||||
assert regions["aws:region1"]["total"] == 5
|
||||
assert regions["aws:region1"]["pass"] == 2
|
||||
assert regions["aws:region1"]["fail"] == 0
|
||||
assert regions["aws:region1"]["muted"] == 3
|
||||
|
||||
# region2 has 4 findings (0 pass, 1 fail, 3 muted)
|
||||
assert regions["aws:region2"]["total"] == 4
|
||||
assert regions["aws:region2"]["pass"] == 0
|
||||
assert regions["aws:region2"]["fail"] == 1
|
||||
assert regions["aws:region2"]["muted"] == 3
|
||||
assert response.status_code == status.HTTP_400_BAD_REQUEST
|
||||
|
||||
def test_overview_services_list(self, authenticated_client, scan_summaries_fixture):
|
||||
response = authenticated_client.get(
|
||||
@@ -6817,14 +6767,15 @@ class TestOverviewViewSet:
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
# Only two different services
|
||||
assert len(response.json()["data"]) == 2
|
||||
# Fixed data from the fixture
|
||||
# Fixed data from the fixture, TODO improve this at some point with something more dynamic
|
||||
service1_data = response.json()["data"][0]
|
||||
service2_data = response.json()["data"][1]
|
||||
assert service1_data["id"] == "service1"
|
||||
assert service2_data["id"] == "service2"
|
||||
|
||||
assert service1_data["attributes"]["total"] == 7
|
||||
assert service2_data["attributes"]["total"] == 2
|
||||
# TODO fix numbers when muted_findings filter is fixed
|
||||
assert service1_data["attributes"]["total"] == 3
|
||||
assert service2_data["attributes"]["total"] == 1
|
||||
|
||||
assert service1_data["attributes"]["pass"] == 1
|
||||
assert service2_data["attributes"]["pass"] == 1
|
||||
@@ -6832,8 +6783,8 @@ class TestOverviewViewSet:
|
||||
assert service1_data["attributes"]["fail"] == 1
|
||||
assert service2_data["attributes"]["fail"] == 0
|
||||
|
||||
assert service1_data["attributes"]["muted"] == 5
|
||||
assert service2_data["attributes"]["muted"] == 1
|
||||
assert service1_data["attributes"]["muted"] == 1
|
||||
assert service2_data["attributes"]["muted"] == 0
|
||||
|
||||
def test_overview_findings_provider_id_in_filter(
|
||||
self, authenticated_client, tenants_fixture, providers_fixture
|
||||
@@ -6943,7 +6894,6 @@ class TestOverviewViewSet:
|
||||
tenant=tenant,
|
||||
)
|
||||
|
||||
# Muted findings should be excluded from severity counts
|
||||
ScanSummary.objects.create(
|
||||
tenant=tenant,
|
||||
scan=scan1,
|
||||
@@ -6953,8 +6903,8 @@ class TestOverviewViewSet:
|
||||
region="region-a",
|
||||
_pass=4,
|
||||
fail=4,
|
||||
muted=3,
|
||||
total=11,
|
||||
muted=0,
|
||||
total=8,
|
||||
)
|
||||
ScanSummary.objects.create(
|
||||
tenant=tenant,
|
||||
@@ -6965,8 +6915,8 @@ class TestOverviewViewSet:
|
||||
region="region-b",
|
||||
_pass=2,
|
||||
fail=2,
|
||||
muted=2,
|
||||
total=6,
|
||||
muted=0,
|
||||
total=4,
|
||||
)
|
||||
ScanSummary.objects.create(
|
||||
tenant=tenant,
|
||||
@@ -6977,8 +6927,8 @@ class TestOverviewViewSet:
|
||||
region="region-c",
|
||||
_pass=1,
|
||||
fail=2,
|
||||
muted=5,
|
||||
total=8,
|
||||
muted=0,
|
||||
total=3,
|
||||
)
|
||||
|
||||
single_response = authenticated_client.get(
|
||||
@@ -6987,7 +6937,6 @@ class TestOverviewViewSet:
|
||||
)
|
||||
assert single_response.status_code == status.HTTP_200_OK
|
||||
single_attributes = single_response.json()["data"]["attributes"]
|
||||
# Should only count pass + fail, excluding muted (3 muted in high, 2 in medium)
|
||||
assert single_attributes["high"] == 8
|
||||
assert single_attributes["medium"] == 4
|
||||
assert single_attributes["critical"] == 0
|
||||
@@ -6998,7 +6947,6 @@ class TestOverviewViewSet:
|
||||
)
|
||||
assert combined_response.status_code == status.HTTP_200_OK
|
||||
combined_attributes = combined_response.json()["data"]["attributes"]
|
||||
# Should only count pass + fail, excluding muted (5 muted in critical)
|
||||
assert combined_attributes["high"] == 8
|
||||
assert combined_attributes["medium"] == 4
|
||||
assert combined_attributes["critical"] == 3
|
||||
|
||||
@@ -158,8 +158,7 @@ def get_prowler_provider_kwargs(
|
||||
|
||||
if mutelist_processor:
|
||||
mutelist_content = mutelist_processor.configuration.get("Mutelist", {})
|
||||
# IaC provider doesn't support mutelist (uses Trivy's built-in logic)
|
||||
if mutelist_content and provider.provider != Provider.ProviderChoices.IAC.value:
|
||||
if mutelist_content:
|
||||
prowler_provider_kwargs["mutelist_content"] = mutelist_content
|
||||
|
||||
return prowler_provider_kwargs
|
||||
|
||||
@@ -2229,30 +2229,6 @@ class OverviewServiceSerializer(serializers.Serializer):
|
||||
return {"version": "v1"}
|
||||
|
||||
|
||||
class OverviewRegionSerializer(serializers.Serializer):
|
||||
id = serializers.SerializerMethodField()
|
||||
provider_type = serializers.CharField()
|
||||
region = serializers.CharField()
|
||||
total = serializers.IntegerField()
|
||||
_pass = serializers.IntegerField()
|
||||
fail = serializers.IntegerField()
|
||||
muted = serializers.IntegerField()
|
||||
|
||||
class JSONAPIMeta:
|
||||
resource_name = "regions-overview"
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.fields["pass"] = self.fields.pop("_pass")
|
||||
|
||||
def get_id(self, obj):
|
||||
"""Generate unique ID from provider_type and region."""
|
||||
return f"{obj['provider_type']}:{obj['region']}"
|
||||
|
||||
def get_root_meta(self, _resource, _many):
|
||||
return {"version": "v1"}
|
||||
|
||||
|
||||
# Schedules
|
||||
|
||||
|
||||
|
||||
@@ -119,6 +119,7 @@ from api.filters import (
|
||||
ScanFilter,
|
||||
ScanSummaryFilter,
|
||||
ScanSummarySeverityFilter,
|
||||
ServiceOverviewFilter,
|
||||
TaskFilter,
|
||||
TenantApiKeyFilter,
|
||||
TenantFilter,
|
||||
@@ -204,7 +205,6 @@ from api.v1.serializers import (
|
||||
OverviewFindingSerializer,
|
||||
OverviewProviderCountSerializer,
|
||||
OverviewProviderSerializer,
|
||||
OverviewRegionSerializer,
|
||||
OverviewServiceSerializer,
|
||||
OverviewSeveritySerializer,
|
||||
ProcessorCreateSerializer,
|
||||
@@ -350,7 +350,7 @@ class SchemaView(SpectacularAPIView):
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
spectacular_settings.TITLE = "Prowler API"
|
||||
spectacular_settings.VERSION = "1.15.2"
|
||||
spectacular_settings.VERSION = "1.15.0"
|
||||
spectacular_settings.DESCRIPTION = (
|
||||
"Prowler API specification.\n\nThis file is auto-generated."
|
||||
)
|
||||
@@ -1662,44 +1662,6 @@ class ProviderViewSet(DisablePaginationMixin, BaseRLSViewSet):
|
||||
),
|
||||
},
|
||||
),
|
||||
ens=extend_schema(
|
||||
tags=["Scan"],
|
||||
summary="Retrieve ENS RD2022 compliance report",
|
||||
description="Download ENS RD2022 compliance report (e.g., 'ens_rd2022_aws') as a PDF file.",
|
||||
request=None,
|
||||
responses={
|
||||
200: OpenApiResponse(
|
||||
description="PDF file containing the ENS compliance report"
|
||||
),
|
||||
202: OpenApiResponse(description="The task is in progress"),
|
||||
401: OpenApiResponse(
|
||||
description="API key missing or user not Authenticated"
|
||||
),
|
||||
403: OpenApiResponse(description="There is a problem with credentials"),
|
||||
404: OpenApiResponse(
|
||||
description="The scan has no ENS reports, or the ENS report generation task has not started yet"
|
||||
),
|
||||
},
|
||||
),
|
||||
nis2=extend_schema(
|
||||
tags=["Scan"],
|
||||
summary="Retrieve NIS2 compliance report",
|
||||
description="Download NIS2 compliance report (Directive (EU) 2022/2555) as a PDF file.",
|
||||
request=None,
|
||||
responses={
|
||||
200: OpenApiResponse(
|
||||
description="PDF file containing the NIS2 compliance report"
|
||||
),
|
||||
202: OpenApiResponse(description="The task is in progress"),
|
||||
401: OpenApiResponse(
|
||||
description="API key missing or user not Authenticated"
|
||||
),
|
||||
403: OpenApiResponse(description="There is a problem with credentials"),
|
||||
404: OpenApiResponse(
|
||||
description="The scan has no NIS2 reports, or the NIS2 report generation task has not started yet"
|
||||
),
|
||||
},
|
||||
),
|
||||
)
|
||||
@method_decorator(CACHE_DECORATOR, name="list")
|
||||
@method_decorator(CACHE_DECORATOR, name="retrieve")
|
||||
@@ -1759,12 +1721,6 @@ class ScanViewSet(BaseRLSViewSet):
|
||||
elif self.action == "threatscore":
|
||||
if hasattr(self, "response_serializer_class"):
|
||||
return self.response_serializer_class
|
||||
elif self.action == "ens":
|
||||
if hasattr(self, "response_serializer_class"):
|
||||
return self.response_serializer_class
|
||||
elif self.action == "nis2":
|
||||
if hasattr(self, "response_serializer_class"):
|
||||
return self.response_serializer_class
|
||||
return super().get_serializer_class()
|
||||
|
||||
def partial_update(self, request, *args, **kwargs):
|
||||
@@ -2018,7 +1974,6 @@ class ScanViewSet(BaseRLSViewSet):
|
||||
if running_resp:
|
||||
return running_resp
|
||||
|
||||
# TODO: add detailed response if the compliance framework is not supported for the provider
|
||||
if not scan.output_location:
|
||||
return Response(
|
||||
{
|
||||
@@ -2047,85 +2002,6 @@ class ScanViewSet(BaseRLSViewSet):
|
||||
content, filename = loader
|
||||
return self._serve_file(content, filename, "application/pdf")
|
||||
|
||||
@action(
|
||||
detail=True,
|
||||
methods=["get"],
|
||||
url_name="ens",
|
||||
)
|
||||
def ens(self, request, pk=None):
|
||||
scan = self.get_object()
|
||||
running_resp = self._get_task_status(scan)
|
||||
if running_resp:
|
||||
return running_resp
|
||||
|
||||
# TODO: add detailed response if the compliance framework is not supported for the provider
|
||||
if not scan.output_location:
|
||||
return Response(
|
||||
{
|
||||
"detail": "The scan has no reports, or the ENS report generation task has not started yet."
|
||||
},
|
||||
status=status.HTTP_404_NOT_FOUND,
|
||||
)
|
||||
|
||||
if scan.output_location.startswith("s3://"):
|
||||
bucket = env.str("DJANGO_OUTPUT_S3_AWS_OUTPUT_BUCKET", "")
|
||||
key_prefix = scan.output_location.removeprefix(f"s3://{bucket}/")
|
||||
prefix = os.path.join(
|
||||
os.path.dirname(key_prefix),
|
||||
"ens",
|
||||
"*_ens_report.pdf",
|
||||
)
|
||||
loader = self._load_file(prefix, s3=True, bucket=bucket, list_objects=True)
|
||||
else:
|
||||
base = os.path.dirname(scan.output_location)
|
||||
pattern = os.path.join(base, "ens", "*_ens_report.pdf")
|
||||
loader = self._load_file(pattern, s3=False)
|
||||
|
||||
if isinstance(loader, Response):
|
||||
return loader
|
||||
|
||||
content, filename = loader
|
||||
return self._serve_file(content, filename, "application/pdf")
|
||||
|
||||
@action(
|
||||
detail=True,
|
||||
methods=["get"],
|
||||
url_name="nis2",
|
||||
)
|
||||
def nis2(self, request, pk=None):
|
||||
scan = self.get_object()
|
||||
running_resp = self._get_task_status(scan)
|
||||
if running_resp:
|
||||
return running_resp
|
||||
|
||||
if not scan.output_location:
|
||||
return Response(
|
||||
{
|
||||
"detail": "The scan has no reports, or the NIS2 report generation task has not started yet."
|
||||
},
|
||||
status=status.HTTP_404_NOT_FOUND,
|
||||
)
|
||||
|
||||
if scan.output_location.startswith("s3://"):
|
||||
bucket = env.str("DJANGO_OUTPUT_S3_AWS_OUTPUT_BUCKET", "")
|
||||
key_prefix = scan.output_location.removeprefix(f"s3://{bucket}/")
|
||||
prefix = os.path.join(
|
||||
os.path.dirname(key_prefix),
|
||||
"nis2",
|
||||
"*_nis2_report.pdf",
|
||||
)
|
||||
loader = self._load_file(prefix, s3=True, bucket=bucket, list_objects=True)
|
||||
else:
|
||||
base = os.path.dirname(scan.output_location)
|
||||
pattern = os.path.join(base, "nis2", "*_nis2_report.pdf")
|
||||
loader = self._load_file(pattern, s3=False)
|
||||
|
||||
if isinstance(loader, Response):
|
||||
return loader
|
||||
|
||||
content, filename = loader
|
||||
return self._serve_file(content, filename, "application/pdf")
|
||||
|
||||
def create(self, request, *args, **kwargs):
|
||||
input_serializer = self.get_serializer(data=request.data)
|
||||
input_serializer.is_valid(raise_exception=True)
|
||||
@@ -2138,7 +2014,7 @@ class ScanViewSet(BaseRLSViewSet):
|
||||
"scan_id": str(scan.id),
|
||||
"provider_id": str(scan.provider_id),
|
||||
# Disabled for now
|
||||
# checks_to_execute=scan.scanner_args.get("checks_to_execute")
|
||||
# checks_to_execute=scan.scanner_args.get("checks_to_execute"),
|
||||
},
|
||||
)
|
||||
|
||||
@@ -3359,50 +3235,15 @@ class RoleProviderGroupRelationshipView(RelationshipView, BaseRLSViewSet):
|
||||
@extend_schema_view(
|
||||
list=extend_schema(
|
||||
tags=["Compliance Overview"],
|
||||
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."
|
||||
),
|
||||
summary="List compliance overviews for a scan",
|
||||
description="Retrieve an overview of all the compliance in a given scan.",
|
||||
parameters=[
|
||||
OpenApiParameter(
|
||||
name="filter[scan_id]",
|
||||
required=False,
|
||||
required=True,
|
||||
type=OpenApiTypes.UUID,
|
||||
location=OpenApiParameter.QUERY,
|
||||
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).",
|
||||
description="Related scan ID.",
|
||||
),
|
||||
],
|
||||
responses={
|
||||
@@ -3723,93 +3564,57 @@ class ComplianceOverviewViewSet(BaseRLSViewSet, TaskManagementMixin):
|
||||
|
||||
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(
|
||||
if not scan_id:
|
||||
raise ValidationError(
|
||||
[
|
||||
{
|
||||
"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,
|
||||
"detail": "This query parameter is required.",
|
||||
"status": 400,
|
||||
"source": {"pointer": "filter[scan_id]"},
|
||||
"code": "required",
|
||||
}
|
||||
)
|
||||
|
||||
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)
|
||||
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,
|
||||
}
|
||||
)
|
||||
|
||||
# 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)
|
||||
serializer = self.get_serializer(response_data, many=True)
|
||||
return Response(serializer.data)
|
||||
|
||||
@action(detail=False, methods=["get"], url_name="metadata")
|
||||
def metadata(self, request):
|
||||
@@ -4061,16 +3866,8 @@ 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."
|
||||
),
|
||||
filters=True,
|
||||
),
|
||||
regions=extend_schema(
|
||||
summary="Get findings data by region",
|
||||
description=(
|
||||
"Retrieve an aggregated summary of findings grouped by region. The response includes the total, passed, "
|
||||
"failed, and muted findings for each region based on the latest completed scans per provider. "
|
||||
"Standard overview filters (inserted_at, provider filters, region filters, etc.) are supported."
|
||||
"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."
|
||||
),
|
||||
filters=True,
|
||||
),
|
||||
@@ -4104,8 +3901,6 @@ class OverviewViewSet(BaseRLSViewSet):
|
||||
return OverviewSeveritySerializer
|
||||
elif self.action == "services":
|
||||
return OverviewServiceSerializer
|
||||
elif self.action == "regions":
|
||||
return OverviewRegionSerializer
|
||||
elif self.action == "threatscore":
|
||||
return ThreatScoreSnapshotSerializer
|
||||
return super().get_serializer_class()
|
||||
@@ -4113,10 +3908,12 @@ class OverviewViewSet(BaseRLSViewSet):
|
||||
def get_filterset_class(self):
|
||||
if self.action == "providers":
|
||||
return None
|
||||
elif self.action in ["findings", "services", "regions"]:
|
||||
elif self.action == "findings":
|
||||
return ScanSummaryFilter
|
||||
elif self.action == "findings_severity":
|
||||
return ScanSummarySeverityFilter
|
||||
elif self.action == "services":
|
||||
return ServiceOverviewFilter
|
||||
return None
|
||||
|
||||
@extend_schema(exclude=True)
|
||||
@@ -4127,35 +3924,6 @@ class OverviewViewSet(BaseRLSViewSet):
|
||||
def retrieve(self, request, *args, **kwargs):
|
||||
raise MethodNotAllowed(method="GET")
|
||||
|
||||
def _get_latest_scans_queryset(self):
|
||||
"""
|
||||
Get filtered queryset for the latest completed scans per provider.
|
||||
|
||||
Returns:
|
||||
Filtered ScanSummary queryset with latest scan IDs applied.
|
||||
"""
|
||||
tenant_id = self.request.tenant_id
|
||||
queryset = self.get_queryset()
|
||||
filtered_queryset = self.filter_queryset(queryset)
|
||||
provider_filter = (
|
||||
{"provider__in": self.allowed_providers}
|
||||
if hasattr(self, "allowed_providers")
|
||||
else {}
|
||||
)
|
||||
|
||||
latest_scan_ids = (
|
||||
Scan.all_objects.filter(
|
||||
tenant_id=tenant_id, state=StateChoices.COMPLETED, **provider_filter
|
||||
)
|
||||
.order_by("provider_id", "-inserted_at")
|
||||
.distinct("provider_id")
|
||||
.values_list("id", flat=True)
|
||||
)
|
||||
|
||||
return filtered_queryset.filter(
|
||||
tenant_id=tenant_id, scan_id__in=latest_scan_ids
|
||||
)
|
||||
|
||||
@action(detail=False, methods=["get"], url_name="providers")
|
||||
def providers(self, request):
|
||||
tenant_id = self.request.tenant_id
|
||||
@@ -4248,7 +4016,26 @@ class OverviewViewSet(BaseRLSViewSet):
|
||||
|
||||
@action(detail=False, methods=["get"], url_name="findings")
|
||||
def findings(self, request):
|
||||
filtered_queryset = self._get_latest_scans_queryset()
|
||||
tenant_id = self.request.tenant_id
|
||||
queryset = self.get_queryset()
|
||||
filtered_queryset = self.filter_queryset(queryset)
|
||||
provider_filter = (
|
||||
{"provider__in": self.allowed_providers}
|
||||
if hasattr(self, "allowed_providers")
|
||||
else {}
|
||||
)
|
||||
|
||||
latest_scan_ids = (
|
||||
Scan.all_objects.filter(
|
||||
tenant_id=tenant_id, state=StateChoices.COMPLETED, **provider_filter
|
||||
)
|
||||
.order_by("provider_id", "-inserted_at")
|
||||
.distinct("provider_id")
|
||||
.values_list("id", flat=True)
|
||||
)
|
||||
filtered_queryset = filtered_queryset.filter(
|
||||
tenant_id=tenant_id, scan_id__in=latest_scan_ids
|
||||
)
|
||||
|
||||
aggregated_totals = filtered_queryset.aggregate(
|
||||
_pass=Sum("_pass") or 0,
|
||||
@@ -4275,14 +4062,37 @@ class OverviewViewSet(BaseRLSViewSet):
|
||||
|
||||
@action(detail=False, methods=["get"], url_name="findings_severity")
|
||||
def findings_severity(self, request):
|
||||
filtered_queryset = self._get_latest_scans_queryset()
|
||||
tenant_id = self.request.tenant_id
|
||||
|
||||
# Load only required fields
|
||||
queryset = self.get_queryset().only(
|
||||
"tenant_id", "scan_id", "severity", "fail", "_pass", "total"
|
||||
)
|
||||
|
||||
filtered_queryset = self.filter_queryset(queryset)
|
||||
provider_filter = (
|
||||
{"provider__in": self.allowed_providers}
|
||||
if hasattr(self, "allowed_providers")
|
||||
else {}
|
||||
)
|
||||
|
||||
latest_scan_ids = (
|
||||
Scan.all_objects.filter(
|
||||
tenant_id=tenant_id, state=StateChoices.COMPLETED, **provider_filter
|
||||
)
|
||||
.order_by("provider_id", "-inserted_at")
|
||||
.distinct("provider_id")
|
||||
.values_list("id", flat=True)
|
||||
)
|
||||
filtered_queryset = filtered_queryset.filter(
|
||||
tenant_id=tenant_id, scan_id__in=latest_scan_ids
|
||||
)
|
||||
|
||||
# The filter will have added a status_count annotation if any status filter was used
|
||||
if "status_count" in filtered_queryset.query.annotations:
|
||||
sum_expression = Sum("status_count")
|
||||
else:
|
||||
# Exclude muted findings by default
|
||||
sum_expression = Sum(F("_pass") + F("fail"))
|
||||
sum_expression = Sum("total")
|
||||
|
||||
severity_counts = (
|
||||
filtered_queryset.values("severity")
|
||||
@@ -4300,7 +4110,26 @@ class OverviewViewSet(BaseRLSViewSet):
|
||||
|
||||
@action(detail=False, methods=["get"], url_name="services")
|
||||
def services(self, request):
|
||||
filtered_queryset = self._get_latest_scans_queryset()
|
||||
tenant_id = self.request.tenant_id
|
||||
queryset = self.get_queryset()
|
||||
filtered_queryset = self.filter_queryset(queryset)
|
||||
provider_filter = (
|
||||
{"provider__in": self.allowed_providers}
|
||||
if hasattr(self, "allowed_providers")
|
||||
else {}
|
||||
)
|
||||
|
||||
latest_scan_ids = (
|
||||
Scan.all_objects.filter(
|
||||
tenant_id=tenant_id, state=StateChoices.COMPLETED, **provider_filter
|
||||
)
|
||||
.order_by("provider_id", "-inserted_at")
|
||||
.distinct("provider_id")
|
||||
.values_list("id", flat=True)
|
||||
)
|
||||
filtered_queryset = filtered_queryset.filter(
|
||||
tenant_id=tenant_id, scan_id__in=latest_scan_ids
|
||||
)
|
||||
|
||||
services_data = (
|
||||
filtered_queryset.values("service")
|
||||
@@ -4315,31 +4144,13 @@ class OverviewViewSet(BaseRLSViewSet):
|
||||
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
|
||||
@action(detail=False, methods=["get"], url_name="regions")
|
||||
def regions(self, request):
|
||||
filtered_queryset = self._get_latest_scans_queryset()
|
||||
|
||||
regions_data = (
|
||||
filtered_queryset.annotate(provider_type=F("scan__provider__provider"))
|
||||
.values("provider_type", "region")
|
||||
.annotate(_pass=Sum("_pass"))
|
||||
.annotate(fail=Sum("fail"))
|
||||
.annotate(muted=Sum("muted"))
|
||||
.annotate(total=Sum("total"))
|
||||
.order_by("provider_type", "region")
|
||||
)
|
||||
|
||||
serializer = self.get_serializer(regions_data, many=True)
|
||||
|
||||
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=["Overview"],
|
||||
tags=["Overviews"],
|
||||
parameters=[
|
||||
OpenApiParameter(
|
||||
name="snapshot_id",
|
||||
|
||||
@@ -1108,8 +1108,8 @@ def scan_summaries_fixture(tenants_fixture, providers_fixture):
|
||||
region="region1",
|
||||
_pass=1,
|
||||
fail=0,
|
||||
muted=2,
|
||||
total=3,
|
||||
muted=0,
|
||||
total=1,
|
||||
new=1,
|
||||
changed=0,
|
||||
unchanged=0,
|
||||
@@ -1117,7 +1117,7 @@ def scan_summaries_fixture(tenants_fixture, providers_fixture):
|
||||
fail_changed=0,
|
||||
pass_new=1,
|
||||
pass_changed=0,
|
||||
muted_new=2,
|
||||
muted_new=0,
|
||||
muted_changed=0,
|
||||
scan=scan,
|
||||
)
|
||||
@@ -1130,8 +1130,8 @@ def scan_summaries_fixture(tenants_fixture, providers_fixture):
|
||||
region="region2",
|
||||
_pass=0,
|
||||
fail=1,
|
||||
muted=3,
|
||||
total=4,
|
||||
muted=1,
|
||||
total=2,
|
||||
new=2,
|
||||
changed=0,
|
||||
unchanged=0,
|
||||
@@ -1139,7 +1139,7 @@ def scan_summaries_fixture(tenants_fixture, providers_fixture):
|
||||
fail_changed=0,
|
||||
pass_new=0,
|
||||
pass_changed=0,
|
||||
muted_new=3,
|
||||
muted_new=1,
|
||||
muted_changed=0,
|
||||
scan=scan,
|
||||
)
|
||||
@@ -1152,8 +1152,8 @@ def scan_summaries_fixture(tenants_fixture, providers_fixture):
|
||||
region="region1",
|
||||
_pass=1,
|
||||
fail=0,
|
||||
muted=1,
|
||||
total=2,
|
||||
muted=0,
|
||||
total=1,
|
||||
new=1,
|
||||
changed=0,
|
||||
unchanged=0,
|
||||
@@ -1161,7 +1161,7 @@ def scan_summaries_fixture(tenants_fixture, providers_fixture):
|
||||
fail_changed=0,
|
||||
pass_new=1,
|
||||
pass_changed=0,
|
||||
muted_new=1,
|
||||
muted_new=0,
|
||||
muted_changed=0,
|
||||
scan=scan,
|
||||
)
|
||||
|
||||
|
Before Width: | Height: | Size: 95 KiB |
|
Before Width: | Height: | Size: 94 KiB |
@@ -15,7 +15,6 @@ from prowler.config.config import (
|
||||
html_file_suffix,
|
||||
json_asff_file_suffix,
|
||||
json_ocsf_file_suffix,
|
||||
set_output_timestamp,
|
||||
)
|
||||
from prowler.lib.outputs.asff.asff import ASFF
|
||||
from prowler.lib.outputs.compliance.aws_well_architected.aws_well_architected import (
|
||||
@@ -59,9 +58,6 @@ from prowler.lib.outputs.compliance.prowler_threatscore.prowler_threatscore_azur
|
||||
from prowler.lib.outputs.compliance.prowler_threatscore.prowler_threatscore_gcp import (
|
||||
ProwlerThreatScoreGCP,
|
||||
)
|
||||
from prowler.lib.outputs.compliance.prowler_threatscore.prowler_threatscore_kubernetes import (
|
||||
ProwlerThreatScoreKubernetes,
|
||||
)
|
||||
from prowler.lib.outputs.compliance.prowler_threatscore.prowler_threatscore_m365 import (
|
||||
ProwlerThreatScoreM365,
|
||||
)
|
||||
@@ -108,10 +104,6 @@ COMPLIANCE_CLASS_MAP = {
|
||||
"kubernetes": [
|
||||
(lambda name: name.startswith("cis_"), KubernetesCIS),
|
||||
(lambda name: name.startswith("iso27001_"), KubernetesISO27001),
|
||||
(
|
||||
lambda name: name == "prowler_threatscore_kubernetes",
|
||||
ProwlerThreatScoreKubernetes,
|
||||
),
|
||||
],
|
||||
"m365": [
|
||||
(lambda name: name.startswith("cis_"), M365CIS),
|
||||
@@ -242,33 +234,36 @@ def _upload_to_s3(
|
||||
logger.error(f"S3 upload failed: {str(e)}")
|
||||
|
||||
|
||||
def _build_output_path(
|
||||
output_directory: str,
|
||||
prowler_provider: str,
|
||||
tenant_id: str,
|
||||
scan_id: str,
|
||||
subdirectory: str = None,
|
||||
) -> str:
|
||||
def _generate_output_directory(
|
||||
output_directory, prowler_provider: object, tenant_id: str, scan_id: str
|
||||
) -> tuple[str, str, str]:
|
||||
"""
|
||||
Build a file system path for the output directory of a prowler scan.
|
||||
Generate a file system path for the output directory of a prowler scan.
|
||||
|
||||
This function constructs the output directory path by combining a base
|
||||
temporary output directory, the tenant ID, the scan ID, and details about
|
||||
the prowler provider along with a timestamp. The resulting path is used to
|
||||
store the output files of a prowler scan.
|
||||
|
||||
Note:
|
||||
This function depends on one external variable:
|
||||
- `output_file_timestamp`: A timestamp (as a string) used to uniquely identify the output.
|
||||
|
||||
Args:
|
||||
output_directory (str): The base output directory.
|
||||
prowler_provider (str): An identifier or descriptor for the prowler provider.
|
||||
Typically, this is a string indicating the provider (e.g., "aws").
|
||||
prowler_provider (object): An identifier or descriptor for the prowler provider.
|
||||
Typically, this is a string indicating the provider (e.g., "aws").
|
||||
tenant_id (str): The unique identifier for the tenant.
|
||||
scan_id (str): The unique identifier for the scan.
|
||||
subdirectory (str, optional): Optional subdirectory to include in the path
|
||||
(e.g., "compliance", "threatscore", "ens").
|
||||
|
||||
Returns:
|
||||
str: The constructed path with directory created.
|
||||
str: The constructed file system path for the prowler scan output directory.
|
||||
|
||||
Example:
|
||||
>>> _build_output_path("/tmp", "aws", "tenant-1234", "scan-5678")
|
||||
'/tmp/tenant-1234/scan-5678/prowler-output-aws-20230215123456'
|
||||
>>> _build_output_path("/tmp", "aws", "tenant-1234", "scan-5678", "threatscore")
|
||||
'/tmp/tenant-1234/scan-5678/threatscore/prowler-output-aws-20230215123456'
|
||||
>>> _generate_output_directory("/tmp", "aws", "tenant-1234", "scan-5678")
|
||||
'/tmp/tenant-1234/aws/scan-5678/prowler-output-2023-02-15T12:34:56',
|
||||
'/tmp/tenant-1234/aws/scan-5678/compliance/prowler-output-2023-02-15T12:34:56'
|
||||
'/tmp/tenant-1234/aws/scan-5678/threatscore/prowler-output-2023-02-15T12:34:56'
|
||||
"""
|
||||
# Sanitize the prowler provider name to ensure it is a valid directory name
|
||||
prowler_provider_sanitized = re.sub(r"[^\w\-]", "-", prowler_provider)
|
||||
@@ -276,107 +271,23 @@ def _build_output_path(
|
||||
with rls_transaction(tenant_id):
|
||||
started_at = Scan.objects.get(id=scan_id).started_at
|
||||
|
||||
set_output_timestamp(started_at)
|
||||
|
||||
timestamp = started_at.strftime("%Y%m%d%H%M%S")
|
||||
|
||||
if subdirectory:
|
||||
path = (
|
||||
f"{output_directory}/{tenant_id}/{scan_id}/{subdirectory}/prowler-output-"
|
||||
f"{prowler_provider_sanitized}-{timestamp}"
|
||||
)
|
||||
else:
|
||||
path = (
|
||||
f"{output_directory}/{tenant_id}/{scan_id}/prowler-output-"
|
||||
f"{prowler_provider_sanitized}-{timestamp}"
|
||||
)
|
||||
|
||||
# Create directory for the path if it doesn't exist
|
||||
path = (
|
||||
f"{output_directory}/{tenant_id}/{scan_id}/prowler-output-"
|
||||
f"{prowler_provider_sanitized}-{timestamp}"
|
||||
)
|
||||
os.makedirs("/".join(path.split("/")[:-1]), exist_ok=True)
|
||||
|
||||
return path
|
||||
|
||||
|
||||
def _generate_compliance_output_directory(
|
||||
output_directory: str,
|
||||
prowler_provider: str,
|
||||
tenant_id: str,
|
||||
scan_id: str,
|
||||
compliance_framework: str,
|
||||
) -> str:
|
||||
"""
|
||||
Generate a file system path for a compliance framework output directory.
|
||||
|
||||
This function constructs the output directory path specifically for a compliance
|
||||
framework (e.g., "threatscore", "ens") by combining a base temporary output directory,
|
||||
the tenant ID, the scan ID, the compliance framework name, and details about the
|
||||
prowler provider along with a timestamp.
|
||||
|
||||
Args:
|
||||
output_directory (str): The base output directory.
|
||||
prowler_provider (str): An identifier or descriptor for the prowler provider.
|
||||
Typically, this is a string indicating the provider (e.g., "aws").
|
||||
tenant_id (str): The unique identifier for the tenant.
|
||||
scan_id (str): The unique identifier for the scan.
|
||||
compliance_framework (str): The compliance framework name (e.g., "threatscore", "ens").
|
||||
|
||||
Returns:
|
||||
str: The path for the compliance framework output directory.
|
||||
|
||||
Example:
|
||||
>>> _generate_compliance_output_directory("/tmp", "aws", "tenant-1234", "scan-5678", "threatscore")
|
||||
'/tmp/tenant-1234/scan-5678/threatscore/prowler-output-aws-20230215123456'
|
||||
>>> _generate_compliance_output_directory("/tmp", "aws", "tenant-1234", "scan-5678", "ens")
|
||||
'/tmp/tenant-1234/scan-5678/ens/prowler-output-aws-20230215123456'
|
||||
>>> _generate_compliance_output_directory("/tmp", "aws", "tenant-1234", "scan-5678", "nis2")
|
||||
'/tmp/tenant-1234/scan-5678/nis2/prowler-output-aws-20230215123456'
|
||||
"""
|
||||
return _build_output_path(
|
||||
output_directory,
|
||||
prowler_provider,
|
||||
tenant_id,
|
||||
scan_id,
|
||||
subdirectory=compliance_framework,
|
||||
compliance_path = (
|
||||
f"{output_directory}/{tenant_id}/{scan_id}/compliance/prowler-output-"
|
||||
f"{prowler_provider_sanitized}-{timestamp}"
|
||||
)
|
||||
os.makedirs("/".join(compliance_path.split("/")[:-1]), exist_ok=True)
|
||||
|
||||
|
||||
def _generate_output_directory(
|
||||
output_directory: str,
|
||||
prowler_provider: str,
|
||||
tenant_id: str,
|
||||
scan_id: str,
|
||||
) -> tuple[str, str]:
|
||||
"""
|
||||
Generate file system paths for the standard and compliance output directories of a prowler scan.
|
||||
|
||||
This function constructs both the standard output directory path and the compliance
|
||||
output directory path by combining a base temporary output directory, the tenant ID,
|
||||
the scan ID, and details about the prowler provider along with a timestamp.
|
||||
|
||||
Args:
|
||||
output_directory (str): The base output directory.
|
||||
prowler_provider (str): An identifier or descriptor for the prowler provider.
|
||||
Typically, this is a string indicating the provider (e.g., "aws").
|
||||
tenant_id (str): The unique identifier for the tenant.
|
||||
scan_id (str): The unique identifier for the scan.
|
||||
|
||||
Returns:
|
||||
tuple[str, str]: A tuple containing (standard_path, compliance_path).
|
||||
|
||||
Example:
|
||||
>>> _generate_output_directory("/tmp", "aws", "tenant-1234", "scan-5678")
|
||||
('/tmp/tenant-1234/scan-5678/prowler-output-aws-20230215123456',
|
||||
'/tmp/tenant-1234/scan-5678/compliance/prowler-output-aws-20230215123456')
|
||||
"""
|
||||
standard_path = _build_output_path(
|
||||
output_directory, prowler_provider, tenant_id, scan_id
|
||||
)
|
||||
compliance_path = _build_output_path(
|
||||
output_directory,
|
||||
prowler_provider,
|
||||
tenant_id,
|
||||
scan_id,
|
||||
subdirectory="compliance",
|
||||
threatscore_path = (
|
||||
f"{output_directory}/{tenant_id}/{scan_id}/threatscore/prowler-output-"
|
||||
f"{prowler_provider_sanitized}-{timestamp}"
|
||||
)
|
||||
os.makedirs("/".join(threatscore_path.split("/")[:-1]), exist_ok=True)
|
||||
|
||||
return standard_path, compliance_path
|
||||
return path, compliance_path, threatscore_path
|
||||
|
||||
@@ -330,7 +330,7 @@ def _create_compliance_summaries(
|
||||
if summary_objects:
|
||||
with rls_transaction(tenant_id):
|
||||
ComplianceOverviewSummary.objects.bulk_create(
|
||||
summary_objects, batch_size=500, ignore_conflicts=True
|
||||
summary_objects, batch_size=500
|
||||
)
|
||||
|
||||
|
||||
@@ -979,14 +979,11 @@ def _aggregate_findings_by_region(
|
||||
findings_count_by_compliance = {}
|
||||
|
||||
with rls_transaction(tenant_id, using=READ_REPLICA_ALIAS):
|
||||
# Fetch only PASS/FAIL findings (optimized query reduces data transfer)
|
||||
# Other statuses are not needed for check_status or ThreatScore calculation
|
||||
# Fetch findings with resources in a single efficient query
|
||||
# Use select_related for finding fields and prefetch_related for many-to-many resources
|
||||
findings = (
|
||||
Finding.all_objects.filter(
|
||||
tenant_id=tenant_id,
|
||||
scan_id=scan_id,
|
||||
muted=False,
|
||||
status__in=["PASS", "FAIL"],
|
||||
tenant_id=tenant_id, scan_id=scan_id, muted=False
|
||||
)
|
||||
.only("id", "check_id", "status", "compliance")
|
||||
.prefetch_related(
|
||||
@@ -1004,8 +1001,6 @@ def _aggregate_findings_by_region(
|
||||
)
|
||||
|
||||
for finding in findings:
|
||||
status = finding.status
|
||||
|
||||
for resource in finding.small_resources:
|
||||
region = resource.region
|
||||
|
||||
@@ -1013,7 +1008,7 @@ def _aggregate_findings_by_region(
|
||||
current_status = check_status_by_region.setdefault(region, {})
|
||||
# Priority: FAIL > any other status
|
||||
if current_status.get(finding.check_id) != "FAIL":
|
||||
current_status[finding.check_id] = status
|
||||
current_status[finding.check_id] = finding.status
|
||||
|
||||
# Aggregate ThreatScore compliance counts
|
||||
if modeled_threatscore_compliance_id in (finding.compliance or {}):
|
||||
@@ -1028,7 +1023,7 @@ def _aggregate_findings_by_region(
|
||||
requirement_id, {"total": 0, "pass": 0}
|
||||
)
|
||||
requirement_stats["total"] += 1
|
||||
if status == "PASS":
|
||||
if finding.status == "PASS":
|
||||
requirement_stats["pass"] += 1
|
||||
|
||||
return check_status_by_region, findings_count_by_compliance
|
||||
|
||||
@@ -1,14 +1,9 @@
|
||||
from collections import defaultdict
|
||||
|
||||
from celery.utils.log import get_task_logger
|
||||
from config.django.base import DJANGO_FINDINGS_BATCH_SIZE
|
||||
from django.db.models import Count, Q
|
||||
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, StatusChoices
|
||||
from prowler.lib.outputs.finding import Finding as FindingOutput
|
||||
|
||||
logger = get_task_logger(__name__)
|
||||
|
||||
@@ -41,15 +36,10 @@ def _aggregate_requirement_statistics_from_database(
|
||||
|
||||
with rls_transaction(tenant_id, using=READ_REPLICA_ALIAS):
|
||||
aggregated_statistics_queryset = (
|
||||
Finding.all_objects.filter(
|
||||
tenant_id=tenant_id, scan_id=scan_id, muted=False
|
||||
)
|
||||
Finding.all_objects.filter(tenant_id=tenant_id, scan_id=scan_id)
|
||||
.values("check_id")
|
||||
.annotate(
|
||||
total_findings=Count(
|
||||
"id",
|
||||
filter=Q(status__in=[StatusChoices.PASS, StatusChoices.FAIL]),
|
||||
),
|
||||
total_findings=Count("id"),
|
||||
passed_findings=Count("id", filter=Q(status=StatusChoices.PASS)),
|
||||
)
|
||||
)
|
||||
@@ -135,105 +125,3 @@ def _calculate_requirements_data_from_statistics(
|
||||
)
|
||||
|
||||
return attributes_by_requirement_id, requirements_list
|
||||
|
||||
|
||||
def _load_findings_for_requirement_checks(
|
||||
tenant_id: str,
|
||||
scan_id: str,
|
||||
check_ids: list[str],
|
||||
prowler_provider,
|
||||
findings_cache: dict[str, list[FindingOutput]] | None = None,
|
||||
) -> dict[str, list[FindingOutput]]:
|
||||
"""
|
||||
Load findings for specific check IDs on-demand with optional caching.
|
||||
|
||||
This function loads only the findings needed for a specific set of checks,
|
||||
minimizing memory usage by avoiding loading all findings at once. This is used
|
||||
when generating detailed findings tables for specific requirements in the PDF.
|
||||
|
||||
Supports optional caching to avoid duplicate queries when generating multiple
|
||||
reports for the same scan.
|
||||
|
||||
Args:
|
||||
tenant_id (str): The tenant ID for Row-Level Security context.
|
||||
scan_id (str): The ID of the scan to retrieve findings for.
|
||||
check_ids (list[str]): List of check IDs to load findings for.
|
||||
prowler_provider: The initialized Prowler provider instance.
|
||||
findings_cache (dict, optional): Cache of already loaded findings.
|
||||
If provided, checks are first looked up in cache before querying database.
|
||||
|
||||
Returns:
|
||||
dict[str, list[FindingOutput]]: Dictionary mapping check_id to list of FindingOutput objects.
|
||||
|
||||
Example:
|
||||
{
|
||||
'aws_iam_user_mfa_enabled': [FindingOutput(...), FindingOutput(...)],
|
||||
'aws_s3_bucket_public_access': [FindingOutput(...)]
|
||||
}
|
||||
"""
|
||||
findings_by_check_id = defaultdict(list)
|
||||
|
||||
if not check_ids:
|
||||
return dict(findings_by_check_id)
|
||||
|
||||
# Initialize cache if not provided
|
||||
if findings_cache is None:
|
||||
findings_cache = {}
|
||||
|
||||
# Separate cached and non-cached check_ids
|
||||
check_ids_to_load = []
|
||||
cache_hits = 0
|
||||
cache_misses = 0
|
||||
|
||||
for check_id in check_ids:
|
||||
if check_id in findings_cache:
|
||||
# Reuse from cache
|
||||
findings_by_check_id[check_id] = findings_cache[check_id]
|
||||
cache_hits += 1
|
||||
else:
|
||||
# Need to load from database
|
||||
check_ids_to_load.append(check_id)
|
||||
cache_misses += 1
|
||||
|
||||
if cache_hits > 0:
|
||||
logger.info(
|
||||
f"Findings cache: {cache_hits} hits, {cache_misses} misses "
|
||||
f"({cache_hits / (cache_hits + cache_misses) * 100:.1f}% hit rate)"
|
||||
)
|
||||
|
||||
# If all check_ids were in cache, return early
|
||||
if not check_ids_to_load:
|
||||
return dict(findings_by_check_id)
|
||||
|
||||
logger.info(f"Loading findings for {len(check_ids_to_load)} checks on-demand")
|
||||
|
||||
findings_queryset = (
|
||||
Finding.all_objects.filter(
|
||||
tenant_id=tenant_id, scan_id=scan_id, check_id__in=check_ids_to_load
|
||||
)
|
||||
.order_by("uid")
|
||||
.iterator()
|
||||
)
|
||||
|
||||
with rls_transaction(tenant_id, using=READ_REPLICA_ALIAS):
|
||||
for batch, is_last_batch in batched(
|
||||
findings_queryset, DJANGO_FINDINGS_BATCH_SIZE
|
||||
):
|
||||
for finding_model in batch:
|
||||
finding_output = FindingOutput.transform_api_finding(
|
||||
finding_model, prowler_provider
|
||||
)
|
||||
findings_by_check_id[finding_output.check_id].append(finding_output)
|
||||
# Update cache with newly loaded findings
|
||||
if finding_output.check_id not in findings_cache:
|
||||
findings_cache[finding_output.check_id] = []
|
||||
findings_cache[finding_output.check_id].append(finding_output)
|
||||
|
||||
total_findings_loaded = sum(
|
||||
len(findings) for findings in findings_by_check_id.values()
|
||||
)
|
||||
logger.info(
|
||||
f"Loaded {total_findings_loaded} findings for {len(findings_by_check_id)} checks"
|
||||
)
|
||||
|
||||
return dict(findings_by_check_id)
|
||||
|
||||
@@ -35,7 +35,7 @@ from tasks.jobs.lighthouse_providers import (
|
||||
refresh_lighthouse_provider_models,
|
||||
)
|
||||
from tasks.jobs.muting import mute_historical_findings
|
||||
from tasks.jobs.report import generate_compliance_reports_job
|
||||
from tasks.jobs.report import generate_threatscore_report_job
|
||||
from tasks.jobs.scan import (
|
||||
aggregate_findings,
|
||||
create_compliance_requirements,
|
||||
@@ -75,8 +75,7 @@ def _perform_scan_complete_tasks(tenant_id: str, scan_id: str, provider_id: str)
|
||||
scan_id=scan_id, provider_id=provider_id, tenant_id=tenant_id
|
||||
),
|
||||
group(
|
||||
# Use optimized task that generates both reports with shared queries
|
||||
generate_compliance_reports_task.si(
|
||||
generate_threatscore_report_task.si(
|
||||
tenant_id=tenant_id, scan_id=scan_id, provider_id=provider_id
|
||||
),
|
||||
check_integrations_task.si(
|
||||
@@ -320,7 +319,7 @@ def generate_outputs_task(scan_id: str, provider_id: str, tenant_id: str):
|
||||
|
||||
frameworks_bulk = Compliance.get_bulk(provider_type)
|
||||
frameworks_avail = get_compliance_frameworks(provider_type)
|
||||
out_dir, comp_dir = _generate_output_directory(
|
||||
out_dir, comp_dir, _ = _generate_output_directory(
|
||||
DJANGO_TMP_OUTPUT_DIRECTORY, provider_uid, tenant_id, scan_id
|
||||
)
|
||||
|
||||
@@ -687,33 +686,19 @@ def jira_integration_task(
|
||||
|
||||
@shared_task(
|
||||
base=RLSTask,
|
||||
name="scan-compliance-reports",
|
||||
name="scan-threatscore-report",
|
||||
queue="scan-reports",
|
||||
)
|
||||
def generate_compliance_reports_task(tenant_id: str, scan_id: str, provider_id: str):
|
||||
def generate_threatscore_report_task(tenant_id: str, scan_id: str, provider_id: str):
|
||||
"""
|
||||
Optimized task to generate ThreatScore, ENS, and NIS2 reports with shared queries.
|
||||
|
||||
This task is more efficient than running separate report tasks because it reuses database queries:
|
||||
- Provider object fetched once (instead of three times)
|
||||
- Requirement statistics aggregated once (instead of three times)
|
||||
- Can reduce database load by up to 50-70%
|
||||
|
||||
Task to generate a threatscore report for a given scan.
|
||||
Args:
|
||||
tenant_id (str): The tenant identifier.
|
||||
scan_id (str): The scan identifier.
|
||||
provider_id (str): The provider identifier.
|
||||
|
||||
Returns:
|
||||
dict: Results for all reports containing upload status and paths.
|
||||
"""
|
||||
return generate_compliance_reports_job(
|
||||
tenant_id=tenant_id,
|
||||
scan_id=scan_id,
|
||||
provider_id=provider_id,
|
||||
generate_threatscore=True,
|
||||
generate_ens=True,
|
||||
generate_nis2=True,
|
||||
return generate_threatscore_report_job(
|
||||
tenant_id=tenant_id, scan_id=scan_id, provider_id=provider_id
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -9,7 +9,6 @@ import pytest
|
||||
from botocore.exceptions import ClientError
|
||||
from tasks.jobs.export import (
|
||||
_compress_output_files,
|
||||
_generate_compliance_output_directory,
|
||||
_generate_output_directory,
|
||||
_upload_to_s3,
|
||||
get_s3_client,
|
||||
@@ -148,11 +147,10 @@ class TestOutputs:
|
||||
)
|
||||
mock_logger.assert_called()
|
||||
|
||||
@patch("tasks.jobs.export.set_output_timestamp")
|
||||
@patch("tasks.jobs.export.rls_transaction")
|
||||
@patch("tasks.jobs.export.Scan")
|
||||
def test_generate_output_directory_creates_paths(
|
||||
self, mock_scan, mock_rls_transaction, mock_set_timestamp, tmpdir
|
||||
self, mock_scan, mock_rls_transaction, tmpdir
|
||||
):
|
||||
# Mock the scan object with a started_at timestamp
|
||||
mock_scan_instance = MagicMock()
|
||||
@@ -170,40 +168,22 @@ class TestOutputs:
|
||||
provider = "aws"
|
||||
expected_timestamp = "20230615103045"
|
||||
|
||||
# Test _generate_output_directory (returns standard and compliance paths)
|
||||
path, compliance = _generate_output_directory(
|
||||
path, compliance, threatscore = _generate_output_directory(
|
||||
base_dir, provider, tenant_id, scan_id
|
||||
)
|
||||
|
||||
assert os.path.isdir(os.path.dirname(path))
|
||||
assert os.path.isdir(os.path.dirname(compliance))
|
||||
assert os.path.isdir(os.path.dirname(threatscore))
|
||||
|
||||
assert path.endswith(f"{provider}-{expected_timestamp}")
|
||||
assert compliance.endswith(f"{provider}-{expected_timestamp}")
|
||||
assert "/compliance/" in compliance
|
||||
|
||||
# Test _generate_compliance_output_directory with "threatscore"
|
||||
threatscore = _generate_compliance_output_directory(
|
||||
base_dir, provider, tenant_id, scan_id, compliance_framework="threatscore"
|
||||
)
|
||||
|
||||
assert os.path.isdir(os.path.dirname(threatscore))
|
||||
assert threatscore.endswith(f"{provider}-{expected_timestamp}")
|
||||
assert "/threatscore/" in threatscore
|
||||
|
||||
# Test _generate_compliance_output_directory with "ens"
|
||||
ens = _generate_compliance_output_directory(
|
||||
base_dir, provider, tenant_id, scan_id, compliance_framework="ens"
|
||||
)
|
||||
|
||||
assert os.path.isdir(os.path.dirname(ens))
|
||||
assert ens.endswith(f"{provider}-{expected_timestamp}")
|
||||
assert "/ens/" in ens
|
||||
|
||||
@patch("tasks.jobs.export.set_output_timestamp")
|
||||
@patch("tasks.jobs.export.rls_transaction")
|
||||
@patch("tasks.jobs.export.Scan")
|
||||
def test_generate_output_directory_invalid_character(
|
||||
self, mock_scan, mock_rls_transaction, mock_set_timestamp, tmpdir
|
||||
self, mock_scan, mock_rls_transaction, tmpdir
|
||||
):
|
||||
# Mock the scan object with a started_at timestamp
|
||||
mock_scan_instance = MagicMock()
|
||||
@@ -221,25 +201,14 @@ class TestOutputs:
|
||||
provider = "aws/test@check"
|
||||
expected_timestamp = "20230615103045"
|
||||
|
||||
# Test provider name sanitization with _generate_output_directory
|
||||
path, compliance = _generate_output_directory(
|
||||
path, compliance, threatscore = _generate_output_directory(
|
||||
base_dir, provider, tenant_id, scan_id
|
||||
)
|
||||
|
||||
assert os.path.isdir(os.path.dirname(path))
|
||||
assert os.path.isdir(os.path.dirname(compliance))
|
||||
assert os.path.isdir(os.path.dirname(threatscore))
|
||||
|
||||
assert path.endswith(f"aws-test-check-{expected_timestamp}")
|
||||
assert compliance.endswith(f"aws-test-check-{expected_timestamp}")
|
||||
|
||||
# Test provider name sanitization with _generate_compliance_output_directory
|
||||
threatscore = _generate_compliance_output_directory(
|
||||
base_dir, provider, tenant_id, scan_id, compliance_framework="threatscore"
|
||||
)
|
||||
ens = _generate_compliance_output_directory(
|
||||
base_dir, provider, tenant_id, scan_id, compliance_framework="ens"
|
||||
)
|
||||
|
||||
assert os.path.isdir(os.path.dirname(threatscore))
|
||||
assert os.path.isdir(os.path.dirname(ens))
|
||||
assert threatscore.endswith(f"aws-test-check-{expected_timestamp}")
|
||||
assert ens.endswith(f"aws-test-check-{expected_timestamp}")
|
||||
|
||||
@@ -3338,10 +3338,7 @@ class TestAggregateFindingsByRegion:
|
||||
|
||||
# Verify filter was called with muted=False
|
||||
mock_findings_filter.assert_called_once_with(
|
||||
tenant_id=tenant_id,
|
||||
scan_id=scan_id,
|
||||
muted=False,
|
||||
status__in=["PASS", "FAIL"],
|
||||
tenant_id=tenant_id, scan_id=scan_id, muted=False
|
||||
)
|
||||
|
||||
@patch("tasks.jobs.scan.Finding.all_objects.filter")
|
||||
|
||||
@@ -109,6 +109,7 @@ class TestGenerateOutputs:
|
||||
return_value=(
|
||||
"/tmp/test/out-dir",
|
||||
"/tmp/test/comp-dir",
|
||||
"/tmp/test/threat-dir",
|
||||
),
|
||||
),
|
||||
patch("tasks.tasks.Scan.all_objects.filter") as mock_scan_update,
|
||||
@@ -138,7 +139,7 @@ class TestGenerateOutputs:
|
||||
patch("tasks.tasks.Finding.all_objects.filter") as mock_findings,
|
||||
patch(
|
||||
"tasks.tasks._generate_output_directory",
|
||||
return_value=("/tmp/test/out", "/tmp/test/comp"),
|
||||
return_value=("/tmp/test/out", "/tmp/test/comp", "/tmp/test/threat"),
|
||||
),
|
||||
patch("tasks.tasks.FindingOutput._transform_findings_stats"),
|
||||
patch("tasks.tasks.FindingOutput.transform_api_finding"),
|
||||
@@ -208,7 +209,7 @@ class TestGenerateOutputs:
|
||||
patch("tasks.tasks.Finding.all_objects.filter") as mock_findings,
|
||||
patch(
|
||||
"tasks.tasks._generate_output_directory",
|
||||
return_value=("/tmp/test/out", "/tmp/test/comp"),
|
||||
return_value=("/tmp/test/out", "/tmp/test/comp", "/tmp/test/threat"),
|
||||
),
|
||||
patch(
|
||||
"tasks.tasks.FindingOutput._transform_findings_stats",
|
||||
@@ -288,6 +289,7 @@ class TestGenerateOutputs:
|
||||
return_value=(
|
||||
"/tmp/test/outdir",
|
||||
"/tmp/test/compdir",
|
||||
"/tmp/test/threatdir",
|
||||
),
|
||||
),
|
||||
patch("tasks.tasks._compress_output_files", return_value="outdir.zip"),
|
||||
@@ -366,6 +368,7 @@ class TestGenerateOutputs:
|
||||
return_value=(
|
||||
"/tmp/test/outdir",
|
||||
"/tmp/test/compdir",
|
||||
"/tmp/test/threatdir",
|
||||
),
|
||||
),
|
||||
patch("tasks.tasks.FindingOutput._transform_findings_stats"),
|
||||
@@ -433,7 +436,7 @@ class TestGenerateOutputs:
|
||||
patch("tasks.tasks.Finding.all_objects.filter") as mock_findings,
|
||||
patch(
|
||||
"tasks.tasks._generate_output_directory",
|
||||
return_value=("/tmp/test/out", "/tmp/test/comp"),
|
||||
return_value=("/tmp/test/out", "/tmp/test/comp", "/tmp/test/threat"),
|
||||
),
|
||||
patch(
|
||||
"tasks.tasks.FindingOutput._transform_findings_stats",
|
||||
@@ -491,7 +494,7 @@ class TestGenerateOutputs:
|
||||
patch("tasks.tasks.Finding.all_objects.filter") as mock_findings,
|
||||
patch(
|
||||
"tasks.tasks._generate_output_directory",
|
||||
return_value=("/tmp/test/out", "/tmp/test/comp"),
|
||||
return_value=("/tmp/test/out", "/tmp/test/comp", "/tmp/test/threat"),
|
||||
),
|
||||
patch("tasks.tasks.FindingOutput._transform_findings_stats"),
|
||||
patch("tasks.tasks.FindingOutput.transform_api_finding"),
|
||||
@@ -532,45 +535,34 @@ class TestScanCompleteTasks:
|
||||
@patch("tasks.tasks.create_compliance_requirements_task.apply_async")
|
||||
@patch("tasks.tasks.perform_scan_summary_task.si")
|
||||
@patch("tasks.tasks.generate_outputs_task.si")
|
||||
@patch("tasks.tasks.generate_compliance_reports_task.si")
|
||||
@patch("tasks.tasks.generate_threatscore_report_task.si")
|
||||
@patch("tasks.tasks.check_integrations_task.si")
|
||||
def test_scan_complete_tasks(
|
||||
self,
|
||||
mock_check_integrations_task,
|
||||
mock_compliance_reports_task,
|
||||
mock_threatscore_task,
|
||||
mock_outputs_task,
|
||||
mock_scan_summary_task,
|
||||
mock_compliance_requirements_task,
|
||||
mock_compliance_tasks,
|
||||
):
|
||||
"""Test that scan complete tasks are properly orchestrated with optimized reports."""
|
||||
_perform_scan_complete_tasks("tenant-id", "scan-id", "provider-id")
|
||||
|
||||
# Verify compliance requirements task is called
|
||||
mock_compliance_requirements_task.assert_called_once_with(
|
||||
mock_compliance_tasks.assert_called_once_with(
|
||||
kwargs={"tenant_id": "tenant-id", "scan_id": "scan-id"},
|
||||
)
|
||||
|
||||
# Verify scan summary task is called
|
||||
mock_scan_summary_task.assert_called_once_with(
|
||||
scan_id="scan-id",
|
||||
tenant_id="tenant-id",
|
||||
)
|
||||
|
||||
# Verify outputs task is called
|
||||
mock_outputs_task.assert_called_once_with(
|
||||
scan_id="scan-id",
|
||||
provider_id="provider-id",
|
||||
tenant_id="tenant-id",
|
||||
)
|
||||
|
||||
# Verify optimized compliance reports task is called (replaces individual tasks)
|
||||
mock_compliance_reports_task.assert_called_once_with(
|
||||
mock_threatscore_task.assert_called_once_with(
|
||||
tenant_id="tenant-id",
|
||||
scan_id="scan-id",
|
||||
provider_id="provider-id",
|
||||
)
|
||||
|
||||
# Verify integrations task is called
|
||||
mock_check_integrations_task.assert_called_once_with(
|
||||
tenant_id="tenant-id",
|
||||
provider_id="provider-id",
|
||||
@@ -746,7 +738,7 @@ class TestCheckIntegrationsTask:
|
||||
mock_initialize_provider.return_value = MagicMock()
|
||||
mock_compliance_bulk.return_value = {}
|
||||
mock_get_frameworks.return_value = []
|
||||
mock_generate_dir.return_value = ("out-dir", "comp-dir")
|
||||
mock_generate_dir.return_value = ("out-dir", "comp-dir", "threat-dir")
|
||||
mock_transform_stats.return_value = {"stats": "data"}
|
||||
|
||||
# Mock findings
|
||||
@@ -871,7 +863,7 @@ class TestCheckIntegrationsTask:
|
||||
mock_initialize_provider.return_value = MagicMock()
|
||||
mock_compliance_bulk.return_value = {}
|
||||
mock_get_frameworks.return_value = []
|
||||
mock_generate_dir.return_value = ("out-dir", "comp-dir")
|
||||
mock_generate_dir.return_value = ("out-dir", "comp-dir", "threat-dir")
|
||||
mock_transform_stats.return_value = {"stats": "data"}
|
||||
|
||||
# Mock findings
|
||||
@@ -987,7 +979,7 @@ class TestCheckIntegrationsTask:
|
||||
mock_initialize_provider.return_value = MagicMock()
|
||||
mock_compliance_bulk.return_value = {}
|
||||
mock_get_frameworks.return_value = []
|
||||
mock_generate_dir.return_value = ("out-dir", "comp-dir")
|
||||
mock_generate_dir.return_value = ("out-dir", "comp-dir", "threat-dir")
|
||||
mock_transform_stats.return_value = {"stats": "data"}
|
||||
|
||||
# Mock findings
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
import warnings
|
||||
|
||||
from dashboard.common_methods import get_section_containers_threatscore
|
||||
|
||||
warnings.filterwarnings("ignore")
|
||||
|
||||
|
||||
def get_table(data):
|
||||
aux = data[
|
||||
[
|
||||
"REQUIREMENTS_ID",
|
||||
"REQUIREMENTS_DESCRIPTION",
|
||||
"REQUIREMENTS_ATTRIBUTES_SECTION",
|
||||
"REQUIREMENTS_ATTRIBUTES_SUBSECTION",
|
||||
"CHECKID",
|
||||
"STATUS",
|
||||
"REGION",
|
||||
"ACCOUNTID",
|
||||
"RESOURCEID",
|
||||
]
|
||||
].copy()
|
||||
|
||||
return get_section_containers_threatscore(
|
||||
aux,
|
||||
"REQUIREMENTS_ATTRIBUTES_SECTION",
|
||||
"REQUIREMENTS_ATTRIBUTES_SUBSECTION",
|
||||
"REQUIREMENTS_ID",
|
||||
)
|
||||
@@ -52,7 +52,7 @@
|
||||
{
|
||||
"group": "Prowler Lighthouse AI",
|
||||
"pages": [
|
||||
"getting-started/products/prowler-lighthouse-ai"
|
||||
"user-guide/tutorials/prowler-app-lighthouse"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -109,13 +109,7 @@
|
||||
"user-guide/tutorials/prowler-app-jira-integration"
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "Lighthouse AI",
|
||||
"pages": [
|
||||
"user-guide/tutorials/prowler-app-lighthouse",
|
||||
"user-guide/tutorials/prowler-app-lighthouse-multi-llm"
|
||||
]
|
||||
},
|
||||
"user-guide/tutorials/prowler-app-lighthouse",
|
||||
"user-guide/tutorials/prowler-cloud-public-ips",
|
||||
{
|
||||
"group": "Tutorials",
|
||||
|
||||
@@ -1,180 +0,0 @@
|
||||
---
|
||||
title: 'Overview'
|
||||
---
|
||||
|
||||
import { VersionBadge } from "/snippets/version-badge.mdx"
|
||||
|
||||
<VersionBadge version="5.8.0" />
|
||||
|
||||
Prowler Lighthouse AI is a Cloud Security Analyst chatbot that helps you understand, prioritize, and remediate security findings in your cloud environments. It's designed to provide security expertise for teams without dedicated resources, acting as your 24/7 virtual cloud security analyst.
|
||||
|
||||
<img src="/images/prowler-app/lighthouse-intro.png" alt="Prowler Lighthouse" />
|
||||
|
||||
<Card title="Set Up Lighthouse AI" icon="rocket" href="/user-guide/tutorials/prowler-app-lighthouse#set-up">
|
||||
Learn how to configure Lighthouse AI with your preferred LLM provider
|
||||
</Card>
|
||||
|
||||
## Capabilities
|
||||
|
||||
Prowler Lighthouse AI is designed to be your AI security team member, with capabilities including:
|
||||
|
||||
### Natural Language Querying
|
||||
|
||||
Ask questions in plain English about your security findings. Examples:
|
||||
|
||||
- "What are my highest risk findings?"
|
||||
- "Show me all S3 buckets with public access."
|
||||
- "What security issues were found in my production accounts?"
|
||||
|
||||
<img src="/images/prowler-app/lighthouse-feature1.png" alt="Natural language querying" />
|
||||
|
||||
### Detailed Remediation Guidance
|
||||
|
||||
Get tailored step-by-step instructions for fixing security issues:
|
||||
|
||||
- Clear explanations of the problem and its impact
|
||||
- Commands or console steps to implement fixes
|
||||
- Alternative approaches with different solutions
|
||||
|
||||
<img src="/images/prowler-app/lighthouse-feature2.png" alt="Detailed Remediation" />
|
||||
|
||||
### Enhanced Context and Analysis
|
||||
|
||||
Lighthouse AI can provide additional context to help you understand the findings:
|
||||
|
||||
- Explain security concepts related to findings in simple terms
|
||||
- Provide risk assessments based on your environment and context
|
||||
- Connect related findings to show broader security patterns
|
||||
|
||||
<img src="/images/prowler-app/lighthouse-config.png" alt="Business Context" />
|
||||
|
||||
<img src="/images/prowler-app/lighthouse-feature3.png" alt="Contextual Responses" />
|
||||
|
||||
## Important Notes
|
||||
|
||||
Prowler Lighthouse AI is powerful, but there are limitations:
|
||||
|
||||
- **Continuous improvement**: Please report any issues, as the feature may make mistakes or encounter errors, despite extensive testing.
|
||||
- **Access limitations**: Lighthouse AI can only access data the logged-in user can view. If you can't see certain information, Lighthouse AI can't see it either.
|
||||
- **NextJS session dependence**: If your Prowler application session expires or logs out, Lighthouse AI will error out. Refresh and log back in to continue.
|
||||
- **Response quality**: The response quality depends on the selected LLM provider and model. Choose models with strong tool-calling capabilities for best results. We recommend `gpt-5` model from OpenAI.
|
||||
|
||||
### Getting Help
|
||||
|
||||
If you encounter issues with Prowler Lighthouse AI or have suggestions for improvements, please [reach out through our Slack channel](https://goto.prowler.com/slack).
|
||||
|
||||
### What Data Is Shared to LLM Providers?
|
||||
|
||||
The following API endpoints are accessible to Prowler Lighthouse AI. Data from the following API endpoints could be shared with LLM provider depending on the scope of user's query:
|
||||
|
||||
#### Accessible API Endpoints
|
||||
|
||||
**User Management:**
|
||||
|
||||
- List all users - `/api/v1/users`
|
||||
- Retrieve the current user's information - `/api/v1/users/me`
|
||||
|
||||
**Provider Management:**
|
||||
|
||||
- List all providers - `/api/v1/providers`
|
||||
- Retrieve data from a provider - `/api/v1/providers/{id}`
|
||||
|
||||
**Scan Management:**
|
||||
|
||||
- List all scans - `/api/v1/scans`
|
||||
- Retrieve data from a specific scan - `/api/v1/scans/{id}`
|
||||
|
||||
**Resource Management:**
|
||||
|
||||
- List all resources - `/api/v1/resources`
|
||||
- Retrieve data for a resource - `/api/v1/resources/{id}`
|
||||
|
||||
**Findings Management:**
|
||||
|
||||
- List all findings - `/api/v1/findings`
|
||||
- Retrieve data from a specific finding - `/api/v1/findings/{id}`
|
||||
- Retrieve metadata values from findings - `/api/v1/findings/metadata`
|
||||
|
||||
**Overview Data:**
|
||||
|
||||
- Get aggregated findings data - `/api/v1/overviews/findings`
|
||||
- Get findings data by severity - `/api/v1/overviews/findings_severity`
|
||||
- Get aggregated provider data - `/api/v1/overviews/providers`
|
||||
- Get findings data by service - `/api/v1/overviews/services`
|
||||
|
||||
**Compliance Management:**
|
||||
|
||||
- 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
|
||||
|
||||
Not all Prowler API endpoints are integrated with Lighthouse AI. They are intentionally excluded for the following reasons:
|
||||
|
||||
- OpenAI/other LLM providers shouldn't have access to sensitive data (like fetching provider secrets and other sensitive config)
|
||||
- Users queries don't need responses from those API endpoints (ex: tasks, tenant details, downloading zip file, etc.)
|
||||
|
||||
**Excluded Endpoints:**
|
||||
|
||||
**User Management:**
|
||||
|
||||
- List specific users information - `/api/v1/users/{id}`
|
||||
- List user memberships - `/api/v1/users/{user_pk}/memberships`
|
||||
- Retrieve membership data from the user - `/api/v1/users/{user_pk}/memberships/{id}`
|
||||
|
||||
**Tenant Management:**
|
||||
|
||||
- List all tenants - `/api/v1/tenants`
|
||||
- Retrieve data from a tenant - `/api/v1/tenants/{id}`
|
||||
- List tenant memberships - `/api/v1/tenants/{tenant_pk}/memberships`
|
||||
- List all invitations - `/api/v1/tenants/invitations`
|
||||
- Retrieve data from tenant invitation - `/api/v1/tenants/invitations/{id}`
|
||||
|
||||
**Security and Configuration:**
|
||||
|
||||
- List all secrets - `/api/v1/providers/secrets`
|
||||
- Retrieve data from a secret - `/api/v1/providers/secrets/{id}`
|
||||
- List all provider groups - `/api/v1/provider-groups`
|
||||
- Retrieve data from a provider group - `/api/v1/provider-groups/{id}`
|
||||
|
||||
**Reports and Tasks:**
|
||||
|
||||
- Download zip report - `/api/v1/scans/{v1}/report`
|
||||
- List all tasks - `/api/v1/tasks`
|
||||
- Retrieve data from a specific task - `/api/v1/tasks/{id}`
|
||||
|
||||
**Lighthouse AI Configuration:**
|
||||
|
||||
- List LLM providers - `/api/v1/lighthouse/providers`
|
||||
- Retrieve LLM provider - `/api/v1/lighthouse/providers/{id}`
|
||||
- List available models - `/api/v1/lighthouse/models`
|
||||
- Retrieve tenant configuration - `/api/v1/lighthouse/configuration`
|
||||
|
||||
<Note>
|
||||
Agents only have access to hit GET endpoints. They don't have access to other HTTP methods.
|
||||
|
||||
</Note>
|
||||
|
||||
## FAQs
|
||||
|
||||
**1. Which LLM providers are supported?**
|
||||
|
||||
Lighthouse AI supports three providers:
|
||||
|
||||
- **OpenAI** - GPT models (GPT-5, GPT-4o, etc.)
|
||||
- **Amazon Bedrock** - Claude, Llama, Titan, and other models via AWS
|
||||
- **OpenAI Compatible** - Custom endpoints like OpenRouter, Ollama, or any OpenAI-compatible service
|
||||
|
||||
For detailed configuration instructions, see [Using Multiple LLM Providers with Lighthouse](/user-guide/tutorials/prowler-app-lighthouse-multi-llm).
|
||||
|
||||
**2. Why a multi-agent supervisor model?**
|
||||
|
||||
Context windows are limited. While demo data fits inside the context window, querying real-world data often exceeds it. A multi-agent architecture is used so different agents fetch different sizes of data and respond with the minimum required data to the supervisor. This spreads the context window usage across agents.
|
||||
|
||||
**3. Is my security data shared with LLM providers?**
|
||||
|
||||
Minimal data is shared to generate useful responses. Agents can access security findings and remediation details when needed. Provider secrets are protected by design and cannot be read. The LLM provider credentials configured with Lighthouse AI are only accessible to our NextJS server and are never sent to the LLM providers. Resource metadata (names, tags, account/project IDs, etc) may be shared with the configured LLM provider based on query requirements.
|
||||
|
||||
**4. Can the Lighthouse AI change my cloud environment?**
|
||||
|
||||
No. The agent doesn't have the tools to make the changes, even if the configured cloud provider API keys contain permissions to modify resources.
|
||||
|
Before Width: | Height: | Size: 96 KiB After Width: | Height: | Size: 197 KiB |
|
Before Width: | Height: | Size: 540 KiB |
|
Before Width: | Height: | Size: 136 KiB After Width: | Height: | Size: 204 KiB |
|
Before Width: | Height: | Size: 147 KiB After Width: | Height: | Size: 241 KiB |
|
Before Width: | Height: | Size: 180 KiB After Width: | Height: | Size: 268 KiB |
|
Before Width: | Height: | Size: 165 KiB After Width: | Height: | Size: 404 KiB |
|
Before Width: | Height: | Size: 347 KiB |
|
Before Width: | Height: | Size: 173 KiB |
@@ -4,6 +4,7 @@ title: "Prowler ThreatScore Documentation"
|
||||
|
||||
|
||||
|
||||
<Info>This feature is only available in Prowler Cloud/App.</Info>
|
||||
|
||||
## Introduction
|
||||
|
||||
|
||||
@@ -14,10 +14,7 @@ Prowler for Google Cloud supports multiple authentication methods. To use a spec
|
||||
Prowler for Google Cloud requires the following permissions:
|
||||
|
||||
### IAM Roles
|
||||
- **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`
|
||||
- **Reader (`roles/reader`)** – Must be granted at the **project, folder, or organization** level to allow scanning of target projects.
|
||||
|
||||
### Project-Level Settings
|
||||
|
||||
@@ -109,46 +106,18 @@ prowler gcp --project-ids <project-id>
|
||||
|
||||
This method uses a service account with a downloaded key file for authentication.
|
||||
|
||||
### Step 1: Create ProwlerRole
|
||||
### Create Service Account and Key
|
||||
|
||||
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.
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
### Step 2: Create the Service Account
|
||||
|
||||
1. Navigate to **IAM & Admin > Service Accounts** and make sure the correct project is selected.
|
||||
|
||||

|
||||
|
||||
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**.
|
||||
|
||||

|
||||
|
||||
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.
|
||||
|
||||

|
||||
|
||||
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**.
|
||||
|
||||

|
||||
|
||||
2. Select **JSON** as the key type and click **Create**. The browser downloads the file exactly once.
|
||||
|
||||

|
||||
|
||||
3. Once created, make sure to store the Key securely.
|
||||
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
|
||||
|
||||
### Using with Prowler CLI
|
||||
|
||||
|
||||
@@ -55,7 +55,6 @@ For Google Cloud, first enter your `GCP Project ID` and then select the authenti
|
||||
- [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" />
|
||||
For detailed instructions on how to setup Service Account authentication, see the [Authentication](/user-guide/providers/gcp/authentication#service-account-authentication) page.
|
||||
</Tab>
|
||||
<Tab title="Application Default Credentials">
|
||||
1. Run the following command in your terminal to authenticate with GCP:
|
||||
@@ -69,7 +68,6 @@ For Google Cloud, first enter your `GCP Project ID` and then select the authenti
|
||||
3. Paste the `Client ID`, `Client Secret` and `Refresh Token` into Prowler App.
|
||||
|
||||
<img src="/images/gcp-credentials.png" alt="GCP Credentials" width="700" />
|
||||
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
|
||||
|
Before Width: | Height: | Size: 185 KiB |
|
Before Width: | Height: | Size: 369 KiB |
|
Before Width: | Height: | Size: 86 KiB |
|
Before Width: | Height: | Size: 587 KiB |
|
Before Width: | Height: | Size: 403 KiB |
|
Before Width: | Height: | Size: 299 KiB |
|
Before Width: | Height: | Size: 472 KiB |
@@ -64,8 +64,6 @@ Prowler App now separates Microsoft 365 authentication into two app-only options
|
||||
|
||||
Use this method whenever possible to avoid managing client secrets and to unlock every Microsoft 365 check, including those that require PowerShell modules.
|
||||
|
||||
For detailed instructions on how to setup Application Certificate Authentication, see the [Authentication](/user-guide/providers/microsoft365/authentication#application-certificate-authentication-recommended) page.
|
||||
|
||||
#### Application Client Secret Authentication
|
||||
|
||||
1. Enter your **tenant ID**: This is the unique identifier for your Microsoft Entra ID directory.
|
||||
@@ -74,7 +72,6 @@ For detailed instructions on how to setup Application Certificate Authentication
|
||||
|
||||
<img src="/images/providers/secret-form.png" alt="M365 client secret authentication form" width="700" />
|
||||
|
||||
For detailed instructions on how to setup Application Client Secret Authentication, see the [Authentication](/user-guide/providers/microsoft365/authentication#application-client-secret-authentication) page.
|
||||
|
||||
### Step 4: Launch the Scan
|
||||
|
||||
|
||||
@@ -1,128 +0,0 @@
|
||||
---
|
||||
title: 'Using Multiple LLM Providers with Lighthouse'
|
||||
---
|
||||
|
||||
import { VersionBadge } from "/snippets/version-badge.mdx"
|
||||
|
||||
<VersionBadge version="5.14.0" />
|
||||
|
||||
Prowler Lighthouse AI supports multiple Large Language Model (LLM) providers, offering flexibility to choose the provider that best fits infrastructure, compliance requirements, and cost considerations. This guide explains how to configure and use different LLM providers with Lighthouse AI.
|
||||
|
||||
## Supported Providers
|
||||
|
||||
Lighthouse AI supports the following LLM providers:
|
||||
|
||||
- **OpenAI**: Provides access to GPT models (GPT-4o, GPT-4, etc.)
|
||||
- **Amazon Bedrock**: Offers AWS-hosted access to Claude, Llama, Titan, and other models
|
||||
- **OpenAI Compatible**: Supports custom endpoints like OpenRouter, Ollama, or any OpenAI-compatible service
|
||||
|
||||
## How Default Providers Work
|
||||
|
||||
All three providers can be configured for a tenant, but only one can be set as the default provider. The first configured provider automatically becomes the default.
|
||||
|
||||
When visiting Lighthouse AI chat, the default provider's default model loads automatically. Users can switch to any available LLM model (including those from non-default providers) using the dropdown in chat.
|
||||
|
||||
<img src="/images/prowler-app/lighthouse-switch-models.png" alt="Switch models in Lighthouse AI chat interface" />
|
||||
|
||||
## Configuring Providers
|
||||
|
||||
Navigate to **Configuration** → **Lighthouse AI** to see all three provider options with a **Connect** button under each.
|
||||
|
||||
<img src="/images/prowler-app/lighthouse-configuration.png" alt="Prowler Lighthouse Configuration" />
|
||||
|
||||
### Connecting a Provider
|
||||
|
||||
To connect a provider:
|
||||
|
||||
1. Click **Connect** under the desired provider
|
||||
2. Enter the required credentials
|
||||
3. Select a default model for that provider
|
||||
4. Click **Connect** to save
|
||||
|
||||
### OpenAI
|
||||
|
||||
#### Required Information
|
||||
|
||||
- **API Key**: OpenAI API key (starts with `sk-` or `sk-proj-`)
|
||||
|
||||
<Note>
|
||||
To generate an OpenAI API key, visit https://platform.openai.com/api-keys
|
||||
</Note>
|
||||
|
||||
### Amazon Bedrock
|
||||
|
||||
#### Required Information
|
||||
|
||||
- **AWS Access Key ID**: AWS access key ID
|
||||
- **AWS Secret Access Key**: AWS secret access key
|
||||
- **AWS Region**: Region where Bedrock is available (e.g., `us-east-1`, `us-west-2`)
|
||||
|
||||
#### Required Permissions
|
||||
|
||||
The AWS user must have the `AmazonBedrockLimitedAccess` managed policy attached:
|
||||
|
||||
```text
|
||||
arn:aws:iam::aws:policy/AmazonBedrockLimitedAccess
|
||||
```
|
||||
|
||||
<Note>
|
||||
Currently, only AWS access key and secret key authentication is supported. Amazon Bedrock API key support will be available soon.
|
||||
</Note>
|
||||
|
||||
<Note>
|
||||
Available models depend on AWS region and account entitlements. Lighthouse AI displays only accessible models.
|
||||
</Note>
|
||||
|
||||
### OpenAI Compatible
|
||||
|
||||
Use this option to connect to any LLM provider exposing OpenAI compatible API endpoint (OpenRouter, Ollama, etc.).
|
||||
|
||||
#### Required Information
|
||||
|
||||
- **API Key**: API key from the compatible service
|
||||
- **Base URL**: API endpoint URL including the API version (e.g., `https://openrouter.ai/api/v1`)
|
||||
|
||||
#### Example: OpenRouter
|
||||
|
||||
1. Create an account at [OpenRouter](https://openrouter.ai/)
|
||||
2. [Generate an API key](https://openrouter.ai/docs/guides/overview/auth/provisioning-api-keys) from the OpenRouter dashboard
|
||||
3. Configure in Lighthouse AI:
|
||||
- **API Key**: OpenRouter API key
|
||||
- **Base URL**: `https://openrouter.ai/api/v1`
|
||||
|
||||
## Changing the Default Provider
|
||||
|
||||
To set a different provider as default:
|
||||
|
||||
1. Navigate to **Configuration** → **Lighthouse AI**
|
||||
2. Click **Configure** under the provider you want as default
|
||||
3. Click **Set as Default**
|
||||
|
||||
<img src="/images/prowler-app/lighthouse-set-default-provider.png" alt="Set default LLM provider" />
|
||||
|
||||
## Updating Provider Credentials
|
||||
|
||||
To update credentials for a connected provider:
|
||||
|
||||
1. Navigate to **Configuration** → **Lighthouse AI**
|
||||
2. Click **Configure** under the provider
|
||||
3. Enter the new credentials
|
||||
4. Click **Update**
|
||||
|
||||
## Deleting a Provider
|
||||
|
||||
To remove a configured provider:
|
||||
|
||||
1. Navigate to **Configuration** → **Lighthouse AI**
|
||||
2. Click **Configure** under the provider
|
||||
3. Click **Delete**
|
||||
|
||||
## Model Recommendations
|
||||
|
||||
For best results with Lighthouse AI, the recommended model is `gpt-5` from OpenAI.
|
||||
|
||||
Models from other providers such as Amazon Bedrock and OpenAI Compatible endpoints can be connected and used, but performance is not guaranteed.
|
||||
|
||||
## Getting Help
|
||||
|
||||
For issues or suggestions, [reach out through our Slack channel](https://goto.prowler.com/slack).
|
||||
@@ -1,20 +1,26 @@
|
||||
---
|
||||
title: 'How It Works'
|
||||
title: 'Prowler Lighthouse AI'
|
||||
---
|
||||
|
||||
import { VersionBadge } from "/snippets/version-badge.mdx"
|
||||
|
||||
<VersionBadge version="5.8.0" />
|
||||
|
||||
Prowler Lighthouse AI integrates Large Language Models (LLMs) with Prowler security findings data.
|
||||
Prowler Lighthouse AI is a Cloud Security Analyst chatbot that helps you understand, prioritize, and remediate security findings in your cloud environments. It's designed to provide security expertise for teams without dedicated resources, acting as your 24/7 virtual cloud security analyst.
|
||||
|
||||
<img src="/images/prowler-app/lighthouse-intro.png" alt="Prowler Lighthouse" />
|
||||
|
||||
## How It Works
|
||||
|
||||
Prowler Lighthouse AI uses OpenAI's language models and integrates with your Prowler security findings data.
|
||||
|
||||
Here's what's happening behind the scenes:
|
||||
|
||||
- The system uses a multi-agent architecture built with [LanggraphJS](https://github.com/langchain-ai/langgraphjs) for LLM logic and [Vercel AI SDK UI](https://sdk.vercel.ai/docs/ai-sdk-ui/overview) for frontend chatbot.
|
||||
- It uses a ["supervisor" architecture](https://langchain-ai.lang.chat/langgraphjs/tutorials/multi_agent/agent_supervisor/) that interacts with different agents for specialized tasks. For example, `findings_agent` can analyze detected security findings, while `overview_agent` provides a summary of connected cloud accounts.
|
||||
- The system connects to the configured LLM provider to understand user's query, fetches the right data, and responds to the query.
|
||||
- The system connects to OpenAI models to understand, fetch the right data, and respond to the user's query.
|
||||
<Note>
|
||||
Lighthouse AI supports multiple LLM providers including OpenAI, Amazon Bedrock, and OpenAI-compatible services. For configuration details, see [Using Multiple LLM Providers with Lighthouse](/user-guide/tutorials/prowler-app-lighthouse-multi-llm).
|
||||
Lighthouse AI is tested against `gpt-4o` and `gpt-4o-mini` OpenAI models.
|
||||
</Note>
|
||||
- The supervisor agent is the main contact point. It is what users interact with directly from the chat interface. It coordinates with other agents to answer users' questions comprehensively.
|
||||
|
||||
@@ -24,22 +30,16 @@ Lighthouse AI supports multiple LLM providers including OpenAI, Amazon Bedrock,
|
||||
All agents can only read relevant security data. They cannot modify your data or access sensitive information like configured secrets or tenant details.
|
||||
|
||||
</Note>
|
||||
|
||||
## Set up
|
||||
|
||||
Getting started with Prowler Lighthouse AI is easy:
|
||||
|
||||
1. Navigate to **Configuration** → **Lighthouse AI**
|
||||
2. Click **Connect** under the desired provider (OpenAI, Amazon Bedrock, or OpenAI Compatible)
|
||||
3. Enter the required credentials
|
||||
4. Select a default model
|
||||
5. Click **Connect** to save
|
||||
1. Go to the configuration page in your Prowler dashboard.
|
||||
2. Enter your OpenAI API key.
|
||||
3. Select your preferred model. The recommended one for best results is `gpt-4o`.
|
||||
4. (Optional) Add business context to improve response quality and prioritization.
|
||||
|
||||
<Note>
|
||||
For detailed configuration instructions for each provider, see [Using Multiple LLM Providers with Lighthouse](/user-guide/tutorials/prowler-app-lighthouse-multi-llm).
|
||||
</Note>
|
||||
|
||||
<img src="/images/prowler-app/lighthouse-configuration.png" alt="Lighthouse AI Configuration" />
|
||||
<img src="/images/prowler-app/lighthouse-config.png" alt="Lighthouse AI Configuration" />
|
||||
|
||||
### Adding Business Context
|
||||
|
||||
@@ -51,3 +51,163 @@ The optional business context field lets you provide additional information to h
|
||||
- Current security initiatives or focus areas
|
||||
|
||||
Better context leads to more relevant responses and prioritization that aligns with your needs.
|
||||
|
||||
## Capabilities
|
||||
|
||||
Prowler Lighthouse AI is designed to be your AI security team member, with capabilities including:
|
||||
|
||||
### Natural Language Querying
|
||||
|
||||
Ask questions in plain English about your security findings. Examples:
|
||||
|
||||
- "What are my highest risk findings?"
|
||||
- "Show me all S3 buckets with public access."
|
||||
- "What security issues were found in my production accounts?"
|
||||
|
||||
<img src="/images/prowler-app/lighthouse-feature1.png" alt="Natural language querying" />
|
||||
|
||||
### Detailed Remediation Guidance
|
||||
|
||||
Get tailored step-by-step instructions for fixing security issues:
|
||||
|
||||
- Clear explanations of the problem and its impact
|
||||
- Commands or console steps to implement fixes
|
||||
- Alternative approaches with different solutions
|
||||
|
||||
<img src="/images/prowler-app/lighthouse-feature2.png" alt="Detailed Remediation" />
|
||||
|
||||
### Enhanced Context and Analysis
|
||||
|
||||
Lighthouse AI can provide additional context to help you understand the findings:
|
||||
|
||||
- Explain security concepts related to findings in simple terms
|
||||
- Provide risk assessments based on your environment and context
|
||||
- Connect related findings to show broader security patterns
|
||||
|
||||
<img src="/images/prowler-app/lighthouse-config.png" alt="Business Context" />
|
||||
|
||||
<img src="/images/prowler-app/lighthouse-feature3.png" alt="Contextual Responses" />
|
||||
|
||||
## Important Notes
|
||||
|
||||
Prowler Lighthouse AI is powerful, but there are limitations:
|
||||
|
||||
- **Continuous improvement**: Please report any issues, as the feature may make mistakes or encounter errors, despite extensive testing.
|
||||
- **Access limitations**: Lighthouse AI can only access data the logged-in user can view. If you can't see certain information, Lighthouse AI can't see it either.
|
||||
- **NextJS session dependence**: If your Prowler application session expires or logs out, Lighthouse AI will error out. Refresh and log back in to continue.
|
||||
- **Response quality**: The response quality depends on the selected OpenAI model. For best results, use gpt-4o.
|
||||
|
||||
### Getting Help
|
||||
|
||||
If you encounter issues with Prowler Lighthouse AI or have suggestions for improvements, please [reach out through our Slack channel](https://goto.prowler.com/slack).
|
||||
|
||||
### What Data Is Shared to OpenAI?
|
||||
|
||||
The following API endpoints are accessible to Prowler Lighthouse AI. Data from the following API endpoints could be shared with OpenAI depending on the scope of user's query:
|
||||
|
||||
#### Accessible API Endpoints
|
||||
|
||||
**User Management:**
|
||||
|
||||
- List all users - `/api/v1/users`
|
||||
- Retrieve the current user's information - `/api/v1/users/me`
|
||||
|
||||
**Provider Management:**
|
||||
|
||||
- List all providers - `/api/v1/providers`
|
||||
- Retrieve data from a provider - `/api/v1/providers/{id}`
|
||||
|
||||
**Scan Management:**
|
||||
|
||||
- List all scans - `/api/v1/scans`
|
||||
- Retrieve data from a specific scan - `/api/v1/scans/{id}`
|
||||
|
||||
**Resource Management:**
|
||||
|
||||
- List all resources - `/api/v1/resources`
|
||||
- Retrieve data for a resource - `/api/v1/resources/{id}`
|
||||
|
||||
**Findings Management:**
|
||||
|
||||
- List all findings - `/api/v1/findings`
|
||||
- Retrieve data from a specific finding - `/api/v1/findings/{id}`
|
||||
- Retrieve metadata values from findings - `/api/v1/findings/metadata`
|
||||
|
||||
**Overview Data:**
|
||||
|
||||
- Get aggregated findings data - `/api/v1/overviews/findings`
|
||||
- Get findings data by severity - `/api/v1/overviews/findings_severity`
|
||||
- Get aggregated provider data - `/api/v1/overviews/providers`
|
||||
- Get findings data by service - `/api/v1/overviews/services`
|
||||
|
||||
**Compliance Management:**
|
||||
|
||||
- List compliance overviews for a scan - `/api/v1/compliance-overviews`
|
||||
- Retrieve data from a specific compliance overview - `/api/v1/compliance-overviews/{id}`
|
||||
|
||||
#### Excluded API Endpoints
|
||||
|
||||
Not all Prowler API endpoints are integrated with Lighthouse AI. They are intentionally excluded for the following reasons:
|
||||
|
||||
- OpenAI/other LLM providers shouldn't have access to sensitive data (like fetching provider secrets and other sensitive config)
|
||||
- Users queries don't need responses from those API endpoints (ex: tasks, tenant details, downloading zip file, etc.)
|
||||
|
||||
**Excluded Endpoints:**
|
||||
|
||||
**User Management:**
|
||||
|
||||
- List specific users information - `/api/v1/users/{id}`
|
||||
- List user memberships - `/api/v1/users/{user_pk}/memberships`
|
||||
- Retrieve membership data from the user - `/api/v1/users/{user_pk}/memberships/{id}`
|
||||
|
||||
**Tenant Management:**
|
||||
|
||||
- List all tenants - `/api/v1/tenants`
|
||||
- Retrieve data from a tenant - `/api/v1/tenants/{id}`
|
||||
- List tenant memberships - `/api/v1/tenants/{tenant_pk}/memberships`
|
||||
- List all invitations - `/api/v1/tenants/invitations`
|
||||
- Retrieve data from tenant invitation - `/api/v1/tenants/invitations/{id}`
|
||||
|
||||
**Security and Configuration:**
|
||||
|
||||
- List all secrets - `/api/v1/providers/secrets`
|
||||
- Retrieve data from a secret - `/api/v1/providers/secrets/{id}`
|
||||
- List all provider groups - `/api/v1/provider-groups`
|
||||
- Retrieve data from a provider group - `/api/v1/provider-groups/{id}`
|
||||
|
||||
**Reports and Tasks:**
|
||||
|
||||
- Download zip report - `/api/v1/scans/{v1}/report`
|
||||
- List all tasks - `/api/v1/tasks`
|
||||
- Retrieve data from a specific task - `/api/v1/tasks/{id}`
|
||||
|
||||
**Lighthouse AI Configuration:**
|
||||
|
||||
- List OpenAI configuration - `/api/v1/lighthouse-config`
|
||||
- Retrieve OpenAI key and configuration - `/api/v1/lighthouse-config/{id}`
|
||||
|
||||
<Note>
|
||||
Agents only have access to hit GET endpoints. They don't have access to other HTTP methods.
|
||||
|
||||
</Note>
|
||||
## FAQs
|
||||
|
||||
**1. Why only OpenAI models?**
|
||||
|
||||
During feature development, we evaluated other LLM models.
|
||||
|
||||
- **Claude AI** - Claude models have [tier-based ratelimits](https://docs.anthropic.com/en/api/rate-limits#requirements-to-advance-tier). For Lighthouse AI to answer slightly complex questions, there are a handful of API calls to the LLM provider within few seconds. With Claude's tiering system, users must purchase $400 credits or convert their subscription to monthly invoicing after talking to their sales team. This pricing may not suit all Prowler users.
|
||||
- **Gemini Models** - Gemini lacks a solid tool calling feature like OpenAI. It calls functions recursively until exceeding limits. Gemini-2.5-Pro-Experimental is better than previous models regarding tool calling and responding, but it's still experimental.
|
||||
- **Deepseek V3** - Doesn't support system prompt messages.
|
||||
|
||||
**2. Why a multi-agent supervisor model?**
|
||||
|
||||
Context windows are limited. While demo data fits inside the context window, querying real-world data often exceeds it. A multi-agent architecture is used so different agents fetch different sizes of data and respond with the minimum required data to the supervisor. This spreads the context window usage across agents.
|
||||
|
||||
**3. Is my security data shared with OpenAI?**
|
||||
|
||||
Minimal data is shared to generate useful responses. Agents can access security findings and remediation details when needed. Provider secrets are protected by design and cannot be read. The OpenAI key configured with Lighthouse AI is only accessible to our NextJS server and is never sent to LLMs. Resource metadata (names, tags, account/project IDs, etc) may be shared with OpenAI based on your query requirements.
|
||||
|
||||
**4. Can the Lighthouse AI change my cloud environment?**
|
||||
|
||||
No. The agent doesn't have the tools to make the changes, even if the configured cloud provider API keys contain permissions to modify resources.
|
||||
|
||||
@@ -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](#user-guide-configuration)**: For organization administrators to configure SAML SSO through Prowler App.
|
||||
- **User Guide**: For organization administrators to configure SAML SSO through Prowler App.
|
||||
|
||||
- **[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.
|
||||
- **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,65 +53,58 @@ On the profile page, find the "SAML SSO Integration" card and click "Enable" to
|
||||
|
||||

|
||||
|
||||
Next section will explain how to fill the IdP configuration based on your Identity Provider.
|
||||
<Info>
|
||||
**Choose Your Method**
|
||||
|
||||
#### Step 3: Configure the Identity Provider (IdP)
|
||||
Choose a Method:
|
||||
**Use Step 3A (Generic Method)** for any SAML 2.0 compliant Identity Provider or when you need custom configuration.
|
||||
|
||||
- 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.
|
||||
**Use Step 3B (Okta App Catalog)** if you're using Okta and want a simplified setup process with pre-configured settings.
|
||||
|
||||
<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.
|
||||
</Info>
|
||||
#### Step 3A: Configure the Identity Provider (IdP) - Generic
|
||||
|
||||
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).
|
||||
Prowler App displays the SAML configuration information needed to configure the IdP. Use this information to create a new SAML application in the IdP.
|
||||
|
||||
To configure the IdP, copy the **ACS URL** and **Audience URI** from Prowler App and use them to set up a new SAML application.
|
||||
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.
|
||||
|
||||
<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).
|
||||
<Info>
|
||||
**IdP Configuration**
|
||||
|
||||
</Info>
|
||||
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).
|
||||
|
||||
</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.
|
||||
</Info>
|
||||
#### Step 3B: Configure Prowler from App Catalog - Okta
|
||||
|
||||
You can find a walkthrough video [here](https://youtu.be/NjSp5owvCdY).
|
||||
Instead of creating a custom SAML integration, Okta administrators can configure Prowler Cloud directly from Okta's application catalog:
|
||||
|
||||
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).
|
||||
|
||||

|
||||

|
||||
|
||||
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.
|
||||
|
||||

|
||||

|
||||
|
||||
3. **Select Prowler Cloud Application**: Click the Prowler Cloud application from the search results to view its details page.
|
||||
3. **Select Prowler Cloud Application**: Click on the Prowler Cloud application from the search results to view its details page.
|
||||
|
||||

|
||||

|
||||
|
||||
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.
|
||||
|
||||

|
||||

|
||||
|
||||
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.
|
||||
|
||||
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>
|
||||
**If you used Step 3B (Okta App Catalog)**, jump to [Step 6: Save and Verify Configuration](#step-6-save-and-verify-configuration).
|
||||
|
||||
#### Step 4: Configure Attribute Mapping in the IdP
|
||||
|
||||
@@ -127,7 +120,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'.
|
||||

|
||||
|
||||
</Info>
|
||||
@@ -163,8 +156,7 @@ 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.
|
||||

|
||||
|
||||
|
||||
@@ -2,11 +2,6 @@
|
||||
|
||||
All notable changes to the **Prowler MCP Server** are documented in this file.
|
||||
|
||||
## [0.1.1] (Prowler 5.14.0)
|
||||
|
||||
### 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,8 +1,9 @@
|
||||
from typing import Any, List
|
||||
from typing import List
|
||||
|
||||
from fastmcp import FastMCP
|
||||
from prowler_mcp_server.prowler_documentation.search_engine import (
|
||||
ProwlerDocsSearchEngine,
|
||||
SearchResult,
|
||||
)
|
||||
|
||||
# Initialize FastMCP server
|
||||
@@ -14,7 +15,7 @@ prowler_docs_search_engine = ProwlerDocsSearchEngine()
|
||||
def search(
|
||||
query: str,
|
||||
page_size: int = 5,
|
||||
) -> List[dict[str, Any]]:
|
||||
) -> List[SearchResult]:
|
||||
"""
|
||||
Search in Prowler documentation.
|
||||
|
||||
|
||||
@@ -2,22 +2,7 @@
|
||||
|
||||
All notable changes to the **Prowler SDK** are documented in this file.
|
||||
|
||||
## [v5.14.2] (Prowler v5.14.2)
|
||||
|
||||
### Fixed
|
||||
- Custom check folder metadata validation [(#9335)](https://github.com/prowler-cloud/prowler/pull/9335)
|
||||
|
||||
---
|
||||
|
||||
## [v5.14.1] (Prowler v5.14.1)
|
||||
|
||||
### Fixed
|
||||
- `sharepoint_external_sharing_managed` check to handle external sharing disabled at organization level [(#9298)](https://github.com/prowler-cloud/prowler/pull/9298)
|
||||
- Support multiple Exchange mailbox policies in M365 `exchange_mailbox_policy_additional_storage_restricted` check [(#9241)](https://github.com/prowler-cloud/prowler/pull/9241)
|
||||
|
||||
---
|
||||
|
||||
## [v5.14.0] (Prowler v5.14.0)
|
||||
## [v5.14.0] (Prowler UNRELEASED)
|
||||
|
||||
### Added
|
||||
- GitHub provider check `organization_default_repository_permission_strict` [(#8785)](https://github.com/prowler-cloud/prowler/pull/8785)
|
||||
@@ -26,23 +11,18 @@ 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)
|
||||
- HIPAA compliance framework for the GCP provider [(#8955)](https://github.com/prowler-cloud/prowler/pull/8955)
|
||||
- Support PDF reporting for ENS compliance framework [(#9158)](https://github.com/prowler-cloud/prowler/pull/9158)
|
||||
- PDF reporting for NIS2 compliance framework [(#9170)](https://github.com/prowler-cloud/prowler/pull/9170)
|
||||
- Add organization ID parameter for MongoDB Atlas provider [(#9167)](https://github.com/prowler-cloud/prowler/pull/9167)
|
||||
- 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 Prowler ThreatScore for the K8S provider [(#9235)](https://github.com/prowler-cloud/prowler/pull/9235)
|
||||
- Add `postgresql_flexible_server_entra_id_authentication_enabled` check for Azure provider [(#8764)](https://github.com/prowler-cloud/prowler/pull/8764)
|
||||
- Add branch name to IaC provider region [(#9296)](https://github.com/prowler-cloud/prowler/pull/9295)
|
||||
|
||||
### Changed
|
||||
- Update AWS Direct Connect service metadata to new format [(#8855)](https://github.com/prowler-cloud/prowler/pull/8855)
|
||||
@@ -76,8 +56,10 @@ All notable changes to the **Prowler SDK** are documented in this file.
|
||||
- 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 Kinesis service metadata to new format [(#9262)](https://github.com/prowler-cloud/prowler/pull/9262)
|
||||
- Update AWS DocumentDB service metadata to new format [(#8862)](https://github.com/prowler-cloud/prowler/pull/8862)
|
||||
|
||||
---
|
||||
|
||||
## [v5.13.2] (Prowler UNRELEASED)
|
||||
|
||||
### Fixed
|
||||
- Check `check_name` has no `resource_name` error for GCP provider [(#9169)](https://github.com/prowler-cloud/prowler/pull/9169)
|
||||
@@ -87,10 +69,6 @@ All notable changes to the **Prowler SDK** are documented in this file.
|
||||
- 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)
|
||||
- Refresh output report timestamps for each scan [(#9272)](https://github.com/prowler-cloud/prowler/pull/9272)
|
||||
- Fix file name parsing for checks on Windows [(#9268)](https://github.com/prowler-cloud/prowler/pull/9268)
|
||||
- Remove typo for Prowler ThreatScore - M365 [(#9274)](https://github.com/prowler-cloud/prowler/pull/9274)
|
||||
- Point HTML logo to the one present in the Github repository [(#9282)](https://github.com/prowler-cloud/prowler/pull/9282)
|
||||
|
||||
---
|
||||
|
||||
@@ -418,7 +396,7 @@ All notable changes to the **Prowler SDK** are documented in this file.
|
||||
|
||||
---
|
||||
|
||||
## [v5.7.5] (Prowler v5.7.5)
|
||||
## [v5.7.5] (Prowler 5.7.5)
|
||||
|
||||
### Fixed
|
||||
- Use unified timestamp for all requirements [(#8059)](https://github.com/prowler-cloud/prowler/pull/8059)
|
||||
|
||||
@@ -24,7 +24,6 @@ from prowler.lib.check.check import (
|
||||
list_checks_json,
|
||||
list_fixers,
|
||||
list_services,
|
||||
load_custom_checks_metadata,
|
||||
parse_checks_from_file,
|
||||
parse_checks_from_folder,
|
||||
print_categories,
|
||||
@@ -91,9 +90,6 @@ from prowler.lib.outputs.compliance.prowler_threatscore.prowler_threatscore_azur
|
||||
from prowler.lib.outputs.compliance.prowler_threatscore.prowler_threatscore_gcp import (
|
||||
ProwlerThreatScoreGCP,
|
||||
)
|
||||
from prowler.lib.outputs.compliance.prowler_threatscore.prowler_threatscore_kubernetes import (
|
||||
ProwlerThreatScoreKubernetes,
|
||||
)
|
||||
from prowler.lib.outputs.compliance.prowler_threatscore.prowler_threatscore_m365 import (
|
||||
ProwlerThreatScoreM365,
|
||||
)
|
||||
@@ -186,11 +182,6 @@ def prowler():
|
||||
logger.debug("Loading checks metadata from .metadata.json files")
|
||||
bulk_checks_metadata = CheckMetadata.get_bulk(provider)
|
||||
|
||||
# Load custom checks metadata before validation
|
||||
if checks_folder:
|
||||
custom_folder_metadata = load_custom_checks_metadata(checks_folder)
|
||||
bulk_checks_metadata.update(custom_folder_metadata)
|
||||
|
||||
if args.list_categories:
|
||||
print_categories(list_categories(bulk_checks_metadata))
|
||||
sys.exit()
|
||||
@@ -854,19 +845,6 @@ def prowler():
|
||||
)
|
||||
generated_outputs["compliance"].append(iso27001)
|
||||
iso27001.batch_write_data_to_file()
|
||||
elif compliance_name == "prowler_threatscore_kubernetes":
|
||||
# Generate Prowler ThreatScore Finding Object
|
||||
filename = (
|
||||
f"{output_options.output_directory}/compliance/"
|
||||
f"{output_options.output_filename}_{compliance_name}.csv"
|
||||
)
|
||||
prowler_threatscore = ProwlerThreatScoreKubernetes(
|
||||
findings=finding_outputs,
|
||||
compliance=bulk_compliance_frameworks[compliance_name],
|
||||
file_path=filename,
|
||||
)
|
||||
generated_outputs["compliance"].append(prowler_threatscore)
|
||||
prowler_threatscore.batch_write_data_to_file()
|
||||
else:
|
||||
filename = (
|
||||
f"{output_options.output_directory}/compliance/"
|
||||
|
||||
@@ -1024,7 +1024,7 @@
|
||||
"Attributes": [
|
||||
{
|
||||
"Title": "AuditDisabled organizationally is set to False",
|
||||
"Section": "3. Logging and Monitoring",
|
||||
"Section": "3. Logging and monitoring",
|
||||
"SubSection": "3.1 Logging",
|
||||
"AttributeDescription": "The setting “Mailbox auditing on by default” determines whether mailbox auditing is automatically enabled across all mailboxes in the organization, regardless of their individual auditing configuration. When this setting is configured as False, it enables auditing at the organization level, overriding the AuditEnabled property for individual mailboxes—even if it is explicitly set to False. With this setting enabled, default audit actions are automatically recorded for all mailboxes without requiring manual configuration. Conversely, disabling this setting (True) effectively turns off mailbox auditing across the organization and overrides any mailbox-level auditing settings. The consequences of disabling this setting include: • Mailbox auditing is completely disabled organization-wide. • No mailbox actions are logged, even if AuditEnabled is set to True for individual mailboxes. • New mailboxes do not inherit auditing, and setting AuditEnabled=True has no effect. • Bypass audit rules set via Set-MailboxAuditBypassAssociation are ignored. • Existing audit records remain in place until they expire based on the audit log retention policy. The recommended configuration is to set this value to False at the organization level to ensure auditing is enforced consistently.",
|
||||
"AdditionalInformation": "Enforcing mailbox auditing by default ensures that audit logging cannot be unintentionally or maliciously disabled on individual mailboxes. This setting provides vital visibility for forensic investigations and incident response (IR) teams, allowing them to trace suspicious or malicious activity—such as unauthorized inbox access, message deletion, or rule manipulation—that may signal account compromise. Consistent auditing across all mailboxes is critical for detecting threat actor behaviors (TTPs) and correlating events across users. While organizations without Microsoft 365 E5 licenses are limited to 90 days of audit log retention, enabling this setting still significantly improves detection and accountability within that window.",
|
||||
@@ -1042,7 +1042,7 @@
|
||||
"Attributes": [
|
||||
{
|
||||
"Title": "Mailbox auditing for E3 users is Enabled",
|
||||
"Section": "3. Logging and Monitoring",
|
||||
"Section": "3. Logging and monitoring",
|
||||
"SubSection": "3.1 Logging",
|
||||
"AttributeDescription": "As of January 2019, Microsoft enables mailbox audit logging by default across all organizations. This feature ensures that specific actions performed by mailbox owners, delegates, and administrators are automatically captured and recorded. These audit records can then be searched by administrators through the mailbox audit log in Microsoft 365. Each mailbox type—whether user, shared, resource, or public folder—can have tailored audit settings to track activities that are most relevant to the organization. While audit logging is enabled by default at the organizational level, it is important to explicitly configure the AuditEnabled property to True on all user mailboxes, and to expand the list of audited actions beyond the Microsoft defaults to meet specific visibility or compliance needs. Note: This recommendation is particularly relevant to users with Microsoft 365 E3 licenses, where audit actions differ slightly from the default configurations in E5.",
|
||||
"AdditionalInformation": "Mailbox auditing plays a critical role in supporting both regulatory compliance and security monitoring. Whether investigating unauthorized configuration changes, potential account compromise, or insider threats, detailed mailbox audit logs provide essential evidence for security operations, forensic analysis, and general administrative oversight. While mailbox auditing is enabled by default for most user mailboxes, certain mailbox types—such as Resource Mailboxes, Public Folder Mailboxes, and the DiscoverySearch Mailbox—do not inherit the organizational auditing default. For these mailboxes, AuditEnabled must be manually set to True to ensure relevant activities are captured. Note: Organizations without Microsoft 365 E5 licenses are subject to a 90-day audit log retention limit, but enabling comprehensive mailbox auditing remains a best practice for operational readiness and incident response.",
|
||||
@@ -1060,7 +1060,7 @@
|
||||
"Attributes": [
|
||||
{
|
||||
"Title": "Mailbox auditing for E5 users is Enabled",
|
||||
"Section": "3. Logging and Monitoring",
|
||||
"Section": "3. Logging and monitoring",
|
||||
"SubSection": "3.1 Logging",
|
||||
"AttributeDescription": "Since January 2019, mailbox audit logging has been enabled by default in all Microsoft 365 organizations. This feature ensures that specific actions performed by mailbox owners, delegates, and administrators are automatically captured and stored as audit records. These logs are accessible to administrators through the Microsoft 365 mailbox audit log, enabling visibility into key mailbox-level activity. Although logging is enabled by default, each mailbox—particularly user and shared mailboxes—can have custom audit actions assigned to capture the specific types of events deemed valuable by the organization. For environments with Microsoft 365 E5 licenses or the advanced auditing add-on, it is recommended to explicitly set AuditEnabled to True on all user mailboxes and to configure additional audit actions beyond Microsoft’s default settings for enhanced visibility. Note: This recommendation specifically applies to E5 or equivalent auditing-enabled license holders, as the available audit depth and event coverage differ from E3.",
|
||||
"AdditionalInformation": "Mailbox audit logging is essential for supporting security investigations, regulatory compliance, and operational forensics in Microsoft 365. Whether you’re tracking unauthorized changes, detecting suspicious access, or conducting post-incident analysis, having a complete and accurate mailbox audit trail is critical. While audit logging is broadly applied by default, certain mailbox types bypass the organizational setting and require manual configuration to enable auditing. These include: • Resource Mailboxes • Public Folder Mailboxes • DiscoverySearch Mailboxes For these mailbox types, the AuditEnabled property must be explicitly set to True to ensure that audit events are captured. Important: Without advanced auditing (included in E5 or via add-on), mailbox audit logs are retained for only 90 days, limiting the historical window for investigations. Nonetheless, enabling detailed auditing remains a key best practice for maintaining strong visibility and compliance readiness.",
|
||||
@@ -1078,7 +1078,7 @@
|
||||
"Attributes": [
|
||||
{
|
||||
"Title": "AuditBypassEnabled is not enabled on mailboxes",
|
||||
"Section": "3. Logging and Monitoring",
|
||||
"Section": "3. Logging and monitoring",
|
||||
"SubSection": "3.1 Logging",
|
||||
"AttributeDescription": "The AuditBypassEnabled setting in Microsoft 365 allows specific user or computer accounts to bypass mailbox audit logging, meaning that any actions they perform on mailboxes will not be recorded in the audit logs. This includes actions such as reading, deleting, moving, or modifying messages.",
|
||||
"AdditionalInformation": "Allowing an account to bypass mailbox audit logging creates a blind spot in security monitoring. If the account is compromised, misused, or maliciously configured, it can access and interact with mailboxes without leaving any trace in the logs. This significantly undermines the organization’s ability to conduct forensic investigations, detect insider threats, or comply with audit requirements.",
|
||||
@@ -1096,7 +1096,7 @@
|
||||
"Attributes": [
|
||||
{
|
||||
"Title": "Microsoft 365 audit log search is Enabled ",
|
||||
"Section": "3. Logging and Monitoring",
|
||||
"Section": "3. Logging and monitoring",
|
||||
"SubSection": "3.2 Retention",
|
||||
"AttributeDescription": "Audit log search in the Microsoft Purview compliance portal allows organizations to track and retain user and administrator activities across Microsoft 365 services. When enabled, audit events—such as sign-ins, file access, configuration changes, and other operational actions—are captured and stored for up to 90 days by default. While some organizations may choose to integrate auditing data with third-party Security Information and Event Management (SIEM) systems, audit log search in Microsoft Purview remains a critical native capability for centralized visibility and incident response. Although global administrators have the ability to disable audit log search, it is generally recommended to keep it enabled to maintain full visibility into user and system activity.",
|
||||
"AdditionalInformation": "Activating audit log search provides essential forensic and compliance value. It enables organizations to detect anomalous behavior, investigate potential security incidents, and demonstrate adherence to regulatory and legal requirements. In addition, it supports operational monitoring, internal audits, and proactive threat detection. By retaining and centralizing audit data within the Microsoft 365 ecosystem, security and compliance teams gain faster access to actionable insights, reducing response times and strengthening the organization’s overall security posture.",
|
||||
@@ -1114,7 +1114,7 @@
|
||||
"Attributes": [
|
||||
{
|
||||
"Title": "Notifications for internal users sending malware is Enabled",
|
||||
"Section": "3. Logging and Monitoring",
|
||||
"Section": "3. Logging and monitoring",
|
||||
"SubSection": "3.3 Monitoring",
|
||||
"AttributeDescription": "Exchange Online Protection (EOP) is Microsoft’s cloud-based email filtering service designed to safeguard organizations against spam, malware, and other email-borne threats. It is included by default in all Microsoft 365 tenants with Exchange Online mailboxes. EOP provides customizable anti-malware policies that allow administrators to define protection settings and configure alerts for detected malicious activity.",
|
||||
"AdditionalInformation": "Enabling notifications for malware detections ensures that administrators are alerted when an internal user sends a message containing malware. Such incidents may signal a compromised user account or infected device, requiring immediate investigation to mitigate potential security breaches.",
|
||||
|
||||
@@ -3,7 +3,6 @@ import pathlib
|
||||
from datetime import datetime, timezone
|
||||
from enum import Enum
|
||||
from os import getcwd
|
||||
from typing import Tuple
|
||||
|
||||
import requests
|
||||
import yaml
|
||||
@@ -11,36 +10,11 @@ from packaging import version
|
||||
|
||||
from prowler.lib.logger import logger
|
||||
|
||||
|
||||
class _MutableTimestamp:
|
||||
"""Lightweight proxy to keep timestamp references in sync across modules."""
|
||||
|
||||
def __init__(self, value: datetime) -> None:
|
||||
self.value = value
|
||||
|
||||
def set(self, value: datetime) -> None:
|
||||
self.value = value
|
||||
|
||||
def __getattr__(self, name):
|
||||
return getattr(self.value, name)
|
||||
|
||||
def __str__(self) -> str: # pragma: no cover - trivial forwarder
|
||||
return str(self.value)
|
||||
|
||||
def __repr__(self) -> str: # pragma: no cover - trivial forwarder
|
||||
return repr(self.value)
|
||||
|
||||
def __eq__(self, other) -> bool:
|
||||
if isinstance(other, _MutableTimestamp):
|
||||
return self.value == other.value
|
||||
return self.value == other
|
||||
|
||||
|
||||
timestamp = _MutableTimestamp(datetime.today())
|
||||
timestamp_utc = _MutableTimestamp(datetime.now(timezone.utc))
|
||||
prowler_version = "5.14.3"
|
||||
timestamp = datetime.today()
|
||||
timestamp_utc = datetime.now(timezone.utc).replace(tzinfo=timezone.utc)
|
||||
prowler_version = "5.14.0"
|
||||
html_logo_url = "https://github.com/prowler-cloud/prowler/"
|
||||
square_logo_img = "https://raw.githubusercontent.com/prowler-cloud/prowler/dc7d2d5aeb92fdf12e8604f42ef6472cd3e8e889/docs/img/prowler-logo-black.png"
|
||||
square_logo_img = "https://prowler.com/wp-content/uploads/logo-html.png"
|
||||
aws_logo = "https://user-images.githubusercontent.com/38561120/235953920-3e3fba08-0795-41dc-b480-9bea57db9f2e.png"
|
||||
azure_logo = "https://user-images.githubusercontent.com/38561120/235927375-b23e2e0f-8932-49ec-b59c-d89f61c8041d.png"
|
||||
gcp_logo = "https://user-images.githubusercontent.com/38561120/235928332-eb4accdc-c226-4391-8e97-6ca86a91cf50.png"
|
||||
@@ -110,34 +84,6 @@ encoding_format_utf_8 = "utf-8"
|
||||
available_output_formats = ["csv", "json-asff", "json-ocsf", "html"]
|
||||
|
||||
|
||||
def set_output_timestamp(
|
||||
new_timestamp: datetime,
|
||||
) -> Tuple[datetime, datetime, str, str]:
|
||||
"""
|
||||
Override the global output timestamps so generated artifacts reflect a specific scan.
|
||||
Returns the previous values so callers can restore them afterwards.
|
||||
"""
|
||||
global timestamp, timestamp_utc, output_file_timestamp, timestamp_iso
|
||||
|
||||
previous_values = (
|
||||
timestamp.value,
|
||||
timestamp_utc.value,
|
||||
output_file_timestamp,
|
||||
timestamp_iso,
|
||||
)
|
||||
|
||||
timestamp.set(new_timestamp)
|
||||
timestamp_utc.set(
|
||||
new_timestamp.astimezone(timezone.utc)
|
||||
if new_timestamp.tzinfo
|
||||
else new_timestamp.replace(tzinfo=timezone.utc)
|
||||
)
|
||||
output_file_timestamp = timestamp.strftime("%Y%m%d%H%M%S")
|
||||
timestamp_iso = timestamp.isoformat(sep=" ", timespec="seconds")
|
||||
|
||||
return previous_values
|
||||
|
||||
|
||||
def get_default_mute_file_path(provider: str):
|
||||
"""
|
||||
get_default_mute_file_path returns the default mute file path for the provider
|
||||
|
||||
@@ -14,7 +14,7 @@ from colorama import Fore, Style
|
||||
import prowler
|
||||
from prowler.config.config import orange_color
|
||||
from prowler.lib.check.custom_checks_metadata import update_check_metadata
|
||||
from prowler.lib.check.models import Check, load_check_metadata
|
||||
from prowler.lib.check.models import Check
|
||||
from prowler.lib.check.utils import recover_checks_from_provider
|
||||
from prowler.lib.logger import logger
|
||||
from prowler.lib.outputs.outputs import report
|
||||
@@ -110,48 +110,6 @@ def parse_checks_from_folder(provider, input_folder: str) -> set:
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def load_custom_checks_metadata(input_folder: str) -> dict:
|
||||
"""
|
||||
Load check metadata from a custom checks folder without copying the checks.
|
||||
This is used to validate check names before the provider is initialized.
|
||||
|
||||
Args:
|
||||
input_folder (str): Path to the folder containing custom checks.
|
||||
|
||||
Returns:
|
||||
dict: A dictionary with CheckID as key and CheckMetadata as value.
|
||||
"""
|
||||
custom_checks_metadata = {}
|
||||
|
||||
try:
|
||||
if not os.path.isdir(input_folder):
|
||||
return custom_checks_metadata
|
||||
|
||||
with os.scandir(input_folder) as checks:
|
||||
for check in checks:
|
||||
if check.is_dir():
|
||||
check_name = check.name
|
||||
metadata_file = os.path.join(
|
||||
input_folder, check_name, f"{check_name}.metadata.json"
|
||||
)
|
||||
if os.path.isfile(metadata_file):
|
||||
try:
|
||||
check_metadata = load_check_metadata(metadata_file)
|
||||
custom_checks_metadata[check_metadata.CheckID] = (
|
||||
check_metadata
|
||||
)
|
||||
except Exception as error:
|
||||
logger.warning(
|
||||
f"Could not load metadata from {metadata_file}: {error}"
|
||||
)
|
||||
return custom_checks_metadata
|
||||
except Exception as error:
|
||||
logger.error(
|
||||
f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}] -- {error}"
|
||||
)
|
||||
return custom_checks_metadata
|
||||
|
||||
|
||||
# Load checks from custom folder
|
||||
def remove_custom_checks_module(input_folder: str, provider: str):
|
||||
# Check if input folder is a S3 URI
|
||||
|
||||
@@ -457,8 +457,7 @@ class Check(ABC, CheckMetadata):
|
||||
# Verify names consistency
|
||||
check_id = self.CheckID
|
||||
class_name = self.__class__.__name__
|
||||
# os.path.basename handles Windows and POSIX paths reliably
|
||||
file_name = os.path.basename(file_path)
|
||||
file_name = file_path.split(sep="/")[-1]
|
||||
|
||||
errors = []
|
||||
if check_id != class_name:
|
||||
|
||||
@@ -117,32 +117,3 @@ class ProwlerThreatScoreM365Model(BaseModel):
|
||||
Muted: bool
|
||||
Framework: str
|
||||
Name: str
|
||||
|
||||
|
||||
class ProwlerThreatScoreKubernetesModel(BaseModel):
|
||||
"""
|
||||
ProwlerThreatScoreKubernetesModel generates a finding's output in Kubernetes Prowler ThreatScore Compliance format.
|
||||
"""
|
||||
|
||||
Provider: str
|
||||
Description: str
|
||||
Context: str
|
||||
Namespace: str
|
||||
AssessmentDate: str
|
||||
Requirements_Id: str
|
||||
Requirements_Description: str
|
||||
Requirements_Attributes_Title: str
|
||||
Requirements_Attributes_Section: str
|
||||
Requirements_Attributes_SubSection: Optional[str] = None
|
||||
Requirements_Attributes_AttributeDescription: str
|
||||
Requirements_Attributes_AdditionalInformation: str
|
||||
Requirements_Attributes_LevelOfRisk: int
|
||||
Requirements_Attributes_Weight: int
|
||||
Status: str
|
||||
StatusExtended: str
|
||||
ResourceId: str
|
||||
ResourceName: str
|
||||
CheckId: str
|
||||
Muted: bool
|
||||
Framework: str
|
||||
Name: str
|
||||
|
||||
@@ -1,98 +0,0 @@
|
||||
from prowler.config.config import timestamp
|
||||
from prowler.lib.check.compliance_models import Compliance
|
||||
from prowler.lib.outputs.compliance.compliance_output import ComplianceOutput
|
||||
from prowler.lib.outputs.compliance.prowler_threatscore.models import (
|
||||
ProwlerThreatScoreKubernetesModel,
|
||||
)
|
||||
from prowler.lib.outputs.finding import Finding
|
||||
|
||||
|
||||
class ProwlerThreatScoreKubernetes(ComplianceOutput):
|
||||
"""
|
||||
This class represents the Kubernetes Prowler ThreatScore compliance output.
|
||||
|
||||
Attributes:
|
||||
- _data (list): A list to store transformed data from findings.
|
||||
- _file_descriptor (TextIOWrapper): A file descriptor to write data to a file.
|
||||
|
||||
Methods:
|
||||
- transform: Transforms findings into Kubernetes Prowler ThreatScore compliance format.
|
||||
"""
|
||||
|
||||
def transform(
|
||||
self,
|
||||
findings: list[Finding],
|
||||
compliance: Compliance,
|
||||
compliance_name: str,
|
||||
) -> None:
|
||||
"""
|
||||
Transforms a list of findings into Kubernetes Prowler ThreatScore compliance format.
|
||||
|
||||
Parameters:
|
||||
- findings (list): A list of findings.
|
||||
- compliance (Compliance): A compliance model.
|
||||
- compliance_name (str): The name of the compliance model.
|
||||
|
||||
Returns:
|
||||
- None
|
||||
"""
|
||||
for finding in findings:
|
||||
# Get the compliance requirements for the finding
|
||||
finding_requirements = finding.compliance.get(compliance_name, [])
|
||||
for requirement in compliance.Requirements:
|
||||
if requirement.Id in finding_requirements:
|
||||
for attribute in requirement.Attributes:
|
||||
compliance_row = ProwlerThreatScoreKubernetesModel(
|
||||
Provider=finding.provider,
|
||||
Description=compliance.Description,
|
||||
Context=finding.account_name,
|
||||
Namespace=finding.region,
|
||||
AssessmentDate=str(timestamp),
|
||||
Requirements_Id=requirement.Id,
|
||||
Requirements_Description=requirement.Description,
|
||||
Requirements_Attributes_Title=attribute.Title,
|
||||
Requirements_Attributes_Section=attribute.Section,
|
||||
Requirements_Attributes_SubSection=attribute.SubSection,
|
||||
Requirements_Attributes_AttributeDescription=attribute.AttributeDescription,
|
||||
Requirements_Attributes_AdditionalInformation=attribute.AdditionalInformation,
|
||||
Requirements_Attributes_LevelOfRisk=attribute.LevelOfRisk,
|
||||
Requirements_Attributes_Weight=attribute.Weight,
|
||||
Status=finding.status,
|
||||
StatusExtended=finding.status_extended,
|
||||
ResourceId=finding.resource_uid,
|
||||
ResourceName=finding.resource_name,
|
||||
CheckId=finding.check_id,
|
||||
Muted=finding.muted,
|
||||
Framework=compliance.Framework,
|
||||
Name=compliance.Name,
|
||||
)
|
||||
self._data.append(compliance_row)
|
||||
# Add manual requirements to the compliance output
|
||||
for requirement in compliance.Requirements:
|
||||
if not requirement.Checks:
|
||||
for attribute in requirement.Attributes:
|
||||
compliance_row = ProwlerThreatScoreKubernetesModel(
|
||||
Provider=compliance.Provider.lower(),
|
||||
Description=compliance.Description,
|
||||
Context="",
|
||||
Namespace="",
|
||||
AssessmentDate=str(timestamp),
|
||||
Requirements_Id=requirement.Id,
|
||||
Requirements_Description=requirement.Description,
|
||||
Requirements_Attributes_Title=attribute.Title,
|
||||
Requirements_Attributes_Section=attribute.Section,
|
||||
Requirements_Attributes_SubSection=attribute.SubSection,
|
||||
Requirements_Attributes_AttributeDescription=attribute.AttributeDescription,
|
||||
Requirements_Attributes_AdditionalInformation=attribute.AdditionalInformation,
|
||||
Requirements_Attributes_LevelOfRisk=attribute.LevelOfRisk,
|
||||
Requirements_Attributes_Weight=attribute.Weight,
|
||||
Status="MANUAL",
|
||||
StatusExtended="Manual check",
|
||||
ResourceId="manual_check",
|
||||
ResourceName="Manual check",
|
||||
CheckId="manual",
|
||||
Muted=False,
|
||||
Framework=compliance.Framework,
|
||||
Name=compliance.Name,
|
||||
)
|
||||
self._data.append(compliance_row)
|
||||
@@ -314,7 +314,9 @@ class Finding(BaseModel):
|
||||
)
|
||||
output_data["resource_uid"] = getattr(check_output, "resource_name", "")
|
||||
# For IaC, resource_line_range only exists on CheckReportIAC, not on Finding objects
|
||||
output_data["region"] = getattr(check_output, "region", "global")
|
||||
output_data["region"] = getattr(
|
||||
check_output, "resource_line_range", "file"
|
||||
)
|
||||
output_data["resource_line_range"] = getattr(
|
||||
check_output, "resource_line_range", ""
|
||||
)
|
||||
|
||||
@@ -241,7 +241,7 @@ class HTML(Output):
|
||||
<th scope="col">Status</th>
|
||||
<th scope="col">Severity</th>
|
||||
<th scope="col">Service Name</th>
|
||||
<th scope="col">Region</th>
|
||||
<th scope="col">{"Line Range" if provider.type == "iac" else "Region"}</th>
|
||||
<th style="width:20%" scope="col">Check ID</th>
|
||||
<th style="width:20%" scope="col">Check Title</th>
|
||||
<th scope="col">Resource ID</th>
|
||||
|
||||
@@ -325,7 +325,7 @@ class Scan:
|
||||
resource_name=report.resource_name,
|
||||
resource_details=report.resource_details,
|
||||
resource_tags={}, # IaC doesn't have resource tags
|
||||
region=report.region, # IaC region is the branch name
|
||||
region="global", # IaC doesn't have regions
|
||||
compliance={}, # IaC doesn't have compliance mappings yet
|
||||
raw=report.resource, # The raw finding dict
|
||||
)
|
||||
|
||||
@@ -1,40 +1,29 @@
|
||||
{
|
||||
"Provider": "aws",
|
||||
"CheckID": "documentdb_cluster_backup_enabled",
|
||||
"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"
|
||||
],
|
||||
"CheckTitle": "Check if DocumentDB Clusters have backup enabled.",
|
||||
"CheckType": [],
|
||||
"ServiceName": "documentdb",
|
||||
"SubServiceName": "",
|
||||
"ResourceIdTemplate": "",
|
||||
"ResourceIdTemplate": "arn:aws:rds:region:account-id:db-cluster",
|
||||
"Severity": "medium",
|
||||
"ResourceType": "AwsRdsDbCluster",
|
||||
"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"
|
||||
],
|
||||
"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",
|
||||
"Remediation": {
|
||||
"Code": {
|
||||
"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```"
|
||||
"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#"
|
||||
},
|
||||
"Recommendation": {
|
||||
"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"
|
||||
"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"
|
||||
}
|
||||
},
|
||||
"Categories": [
|
||||
"resilience"
|
||||
],
|
||||
"Categories": [],
|
||||
"DependsOn": [],
|
||||
"RelatedTo": [],
|
||||
"Notes": ""
|
||||
|
||||
@@ -1,39 +1,29 @@
|
||||
{
|
||||
"Provider": "aws",
|
||||
"CheckID": "documentdb_cluster_cloudwatch_log_export",
|
||||
"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"
|
||||
],
|
||||
"CheckTitle": "Check if DocumentDB clusters are using the log export feature.",
|
||||
"CheckType": [],
|
||||
"ServiceName": "documentdb",
|
||||
"SubServiceName": "",
|
||||
"ResourceIdTemplate": "",
|
||||
"ResourceIdTemplate": "arn:partition:service:region:account-id:resource-id",
|
||||
"Severity": "medium",
|
||||
"ResourceType": "AwsRdsDbCluster",
|
||||
"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"
|
||||
],
|
||||
"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",
|
||||
"Remediation": {
|
||||
"Code": {
|
||||
"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```"
|
||||
"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"
|
||||
},
|
||||
"Recommendation": {
|
||||
"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"
|
||||
"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"
|
||||
}
|
||||
},
|
||||
"Categories": [
|
||||
"logging"
|
||||
],
|
||||
"Categories": [],
|
||||
"DependsOn": [],
|
||||
"RelatedTo": [],
|
||||
"Notes": ""
|
||||
|
||||
@@ -1,40 +1,29 @@
|
||||
{
|
||||
"Provider": "aws",
|
||||
"CheckID": "documentdb_cluster_deletion_protection",
|
||||
"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"
|
||||
],
|
||||
"CheckTitle": "Check if DocumentDB Clusters has deletion protection enabled.",
|
||||
"CheckType": [],
|
||||
"ServiceName": "documentdb",
|
||||
"SubServiceName": "",
|
||||
"ResourceIdTemplate": "",
|
||||
"ResourceIdTemplate": "arn:aws:rds:region:account-id:db-cluster",
|
||||
"Severity": "medium",
|
||||
"ResourceType": "AwsRdsDbCluster",
|
||||
"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"
|
||||
],
|
||||
"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",
|
||||
"Remediation": {
|
||||
"Code": {
|
||||
"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```"
|
||||
"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#"
|
||||
},
|
||||
"Recommendation": {
|
||||
"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"
|
||||
"Text": "Enable deletion protection for production DocumentDB Clusters.",
|
||||
"Url": "https://docs.aws.amazon.com/securityhub/latest/userguide/documentdb-controls.html#documentdb-5"
|
||||
}
|
||||
},
|
||||
"Categories": [
|
||||
"resilience"
|
||||
],
|
||||
"Categories": [],
|
||||
"DependsOn": [],
|
||||
"RelatedTo": [],
|
||||
"Notes": ""
|
||||
|
||||
@@ -1,36 +1,30 @@
|
||||
{
|
||||
"Provider": "aws",
|
||||
"CheckID": "documentdb_cluster_multi_az_enabled",
|
||||
"CheckTitle": "DocumentDB cluster has Multi-AZ enabled",
|
||||
"CheckType": [
|
||||
"Software and Configuration Checks/AWS Security Best Practices"
|
||||
],
|
||||
"CheckTitle": "Ensure DocumentDB Cluster have Multi-AZ enabled.",
|
||||
"CheckType": [],
|
||||
"ServiceName": "documentdb",
|
||||
"SubServiceName": "",
|
||||
"ResourceIdTemplate": "",
|
||||
"ResourceIdTemplate": "arn:aws:rds:region:account-id:db-cluster",
|
||||
"Severity": "medium",
|
||||
"ResourceType": "AwsRdsDbCluster",
|
||||
"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"
|
||||
],
|
||||
"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",
|
||||
"Remediation": {
|
||||
"Code": {
|
||||
"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```"
|
||||
"CLI": "",
|
||||
"NativeIaC": "",
|
||||
"Other": "",
|
||||
"Terraform": ""
|
||||
},
|
||||
"Recommendation": {
|
||||
"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"
|
||||
"Text": "Enable Multi-AZ for all DocumentDB Clusters.",
|
||||
"Url": "https://docs.aws.amazon.com/documentdb/latest/developerguide/failover.html"
|
||||
}
|
||||
},
|
||||
"Categories": [
|
||||
"resilience"
|
||||
"redundancy"
|
||||
],
|
||||
"DependsOn": [],
|
||||
"RelatedTo": [],
|
||||
|
||||
@@ -1,36 +1,26 @@
|
||||
{
|
||||
"Provider": "aws",
|
||||
"CheckID": "documentdb_cluster_public_snapshot",
|
||||
"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"
|
||||
],
|
||||
"CheckTitle": "Check if DocumentDB manual cluster snapshot is public.",
|
||||
"CheckType": [],
|
||||
"ServiceName": "documentdb",
|
||||
"SubServiceName": "",
|
||||
"ResourceIdTemplate": "",
|
||||
"ResourceIdTemplate": "arn:partition:service:region:account-id:resource-id",
|
||||
"Severity": "critical",
|
||||
"ResourceType": "AwsRdsDbClusterSnapshot",
|
||||
"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"
|
||||
],
|
||||
"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",
|
||||
"Remediation": {
|
||||
"Code": {
|
||||
"CLI": "aws docdb modify-db-cluster-snapshot-attribute --db-cluster-snapshot-identifier <snapshot_id> --attribute-name restore --values-to-remove all",
|
||||
"CLI": "aws docdb modify-db-snapshot-attribute --db-snapshot-identifier <snapshot_id> --attribute-name restore --values-to-remove all",
|
||||
"NativeIaC": "",
|
||||
"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",
|
||||
"Other": "https://docs.aws.amazon.com/securityhub/latest/userguide/documentdb-controls.html#documentdb-3",
|
||||
"Terraform": ""
|
||||
},
|
||||
"Recommendation": {
|
||||
"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"
|
||||
"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"
|
||||
}
|
||||
},
|
||||
"Categories": [
|
||||
|
||||
@@ -1,44 +1,31 @@
|
||||
{
|
||||
"Provider": "aws",
|
||||
"CheckID": "documentdb_cluster_storage_encrypted",
|
||||
"CheckTitle": "DocumentDB cluster storage is encrypted at rest",
|
||||
"CheckTitle": "Check if DocumentDB cluster storage is encrypted.",
|
||||
"CheckType": [
|
||||
"Software and Configuration Checks/AWS Security Best Practices",
|
||||
"Software and Configuration Checks/Industry and Regulatory Standards/AWS Foundational Security Best Practices",
|
||||
"Software and Configuration Checks/Industry and Regulatory Standards/PCI-DSS",
|
||||
"Software and Configuration Checks/Industry and Regulatory Standards/HIPAA Controls (USA)",
|
||||
"Software and Configuration Checks/Industry and Regulatory Standards/NIST 800-53 Controls (USA)",
|
||||
"Software and Configuration Checks/Industry and Regulatory Standards/ISO 27001 Controls",
|
||||
"Effects/Data Exposure"
|
||||
"Data Protection"
|
||||
],
|
||||
"ServiceName": "documentdb",
|
||||
"SubServiceName": "",
|
||||
"ResourceIdTemplate": "",
|
||||
"ResourceIdTemplate": "arn:partition:service:region:account-id:resource-id",
|
||||
"Severity": "medium",
|
||||
"ResourceType": "AwsRdsDbCluster",
|
||||
"Description": "**Amazon DocumentDB clusters** are assessed for **storage encryption at rest** via the cluster's `encrypted` setting.\n\nIt identifies clusters where data volumes, automated backups, and snapshots aren't protected by AWS KMS-managed encryption.",
|
||||
"Risk": "Without at-rest encryption, cluster data, snapshots, and backups can be read in plaintext if copies are leaked, mis-shared, or underlying storage is accessed. This harms **confidentiality**, enables offline analysis and data exfiltration, and widens the blast radius of insider or backup repository compromise.",
|
||||
"RelatedUrl": "",
|
||||
"AdditionalURLs": [
|
||||
"https://docs.aws.amazon.com/securityhub/latest/userguide/documentdb-controls.html#documentdb-1",
|
||||
"https://docs.aws.amazon.com/documentdb/latest/developerguide/elastic-encryption.html",
|
||||
"https://docs.aws.amazon.com/documentdb/latest/developerguide/encryption-at-rest.html"
|
||||
],
|
||||
"Description": "Check if DocumentDB cluster storage is encrypted.",
|
||||
"Risk": "Ensure that encryption of data at rest is enabled for your Amazon DocumentDB (with MongoDB compatibility) database clusters for additional data security and regulatory compliance.",
|
||||
"RelatedUrl": "https://docs.aws.amazon.com/securityhub/latest/userguide/documentdb-controls.html#documentdb-1",
|
||||
"Remediation": {
|
||||
"Code": {
|
||||
"CLI": "aws docdb create-db-cluster --db-cluster-identifier <DB_CLUSTER_ID> --engine docdb --master-username <MASTER_USERNAME> --master-user-password <MASTER_PASSWORD> --storage-encrypted",
|
||||
"NativeIaC": "```yaml\n# CloudFormation: Create an encrypted DocumentDB cluster\nResources:\n <example_resource_name>:\n Type: AWS::DocDB::DBCluster\n Properties:\n Engine: docdb\n MasterUsername: <MASTER_USERNAME>\n MasterUserPassword: <MASTER_PASSWORD>\n StorageEncrypted: true # Critical: enables encryption at rest to pass the check\n```",
|
||||
"Other": "1. In the AWS Console, go to Amazon DocumentDB\n2. Click Create cluster\n3. Expand Show advanced settings\n4. In Encryption-at-rest, select Enable encryption\n5. Choose or keep the default KMS key\n6. Click Create cluster\n\nTo replace an existing unencrypted cluster:\n1. Select the unencrypted cluster > Actions > Take snapshot\n2. After the snapshot completes, select it > Actions > Restore snapshot\n3. In Encryption-at-rest, select Enable encryption and restore as a new cluster\n4. Update your applications to use the new cluster endpoint",
|
||||
"Terraform": "```hcl\n# Terraform: Encrypted DocumentDB cluster\nresource \"aws_docdb_cluster\" \"<example_resource_name>\" {\n master_username = \"<MASTER_USERNAME>\"\n master_password = \"<MASTER_PASSWORD>\"\n storage_encrypted = true # Critical: enables encryption at rest to pass the check\n}\n```"
|
||||
"CLI": "aws docdb create-db-cluster --db-cluster-identifier <DB_CLUSTER_ID> --port <PORT> --engine docdb --master-username <MASTER_USERNAME> --master-user-password <MASTER_PASSWORD> --storage-encrypted",
|
||||
"NativeIaC": "",
|
||||
"Other": "https://docs.prowler.com/checks/aws/general-policies/bc_aws_general_28/",
|
||||
"Terraform": "https://docs.prowler.com/checks/aws/general-policies/bc_aws_general_28#fix-buildtime"
|
||||
},
|
||||
"Recommendation": {
|
||||
"Text": "Enable **storage encryption at rest** for all DocumentDB clusters and prefer **customer-managed KMS keys** for control over access, rotation, and revocation. Apply **least privilege** to key usage, enforce **separation of duties**, and monitor key and snapshot access. *If a cluster isn't encrypted*, migrate to a new encrypted cluster.",
|
||||
"Url": "https://hub.prowler.com/check/documentdb_cluster_storage_encrypted"
|
||||
"Text": "Enable Encryption. Use a CMK where possible. It will provide additional management and privacy benefits.",
|
||||
"Url": "https://docs.aws.amazon.com/securityhub/latest/userguide/documentdb-controls.html#documentdb-1"
|
||||
}
|
||||
},
|
||||
"Categories": [
|
||||
"encryption"
|
||||
],
|
||||
"Categories": [],
|
||||
"DependsOn": [],
|
||||
"RelatedTo": [],
|
||||
"Notes": ""
|
||||
|
||||
@@ -1,39 +1,31 @@
|
||||
{
|
||||
"Provider": "aws",
|
||||
"CheckID": "kinesis_stream_data_retention_period",
|
||||
"CheckTitle": "Kinesis stream retains data for at least the required minimum hours",
|
||||
"CheckTitle": "Kinesis streams should have an adequate data retention period.",
|
||||
"CheckType": [
|
||||
"Software and Configuration Checks/AWS Security Best Practices",
|
||||
"Software and Configuration Checks/Industry and Regulatory Standards/AWS Foundational Security Best Practices",
|
||||
"Effects/Data Destruction"
|
||||
"Software and Configuration Checks/Industry and Regulatory Standards/AWS Foundational Security Best Practices"
|
||||
],
|
||||
"ServiceName": "kinesis",
|
||||
"SubServiceName": "",
|
||||
"ResourceIdTemplate": "",
|
||||
"ResourceIdTemplate": "arn:partition:kinesis::account-id:stream/stream-name",
|
||||
"Severity": "medium",
|
||||
"ResourceType": "AwsKinesisStream",
|
||||
"Description": "**Kinesis Data Streams** retention window is evaluated to confirm records are kept for at least the configured minimum duration (default `168` hours).",
|
||||
"Risk": "Insufficient retention causes records to expire before consumers read or reprocess them, undermining **availability** and analytics **integrity**. Backlogs or outages can create irreversible data gaps, hinder investigations and recovery, and enable denial-of-service-by-lag against event pipelines.",
|
||||
"RelatedUrl": "",
|
||||
"AdditionalURLs": [
|
||||
"https://docs.aws.amazon.com/streams/latest/dev/kinesis-extended-retention.html",
|
||||
"https://docs.aws.amazon.com/securityhub/latest/userguide/kinesis-controls.html#kinesis-3"
|
||||
],
|
||||
"Description": "Ensure Kinesis streams have an adequate data retention period.",
|
||||
"Risk": "An inadequate data retention period may result in data records being deleted before they can be processed or backed up, increasing the risk of data loss. This is especially critical for applications that rely on historical data availability for analysis, monitoring, and recovery in case of failures.",
|
||||
"RelatedUrl": "https://docs.aws.amazon.com/config/latest/developerguide/kinesis-stream-backup-retention-check.html",
|
||||
"Remediation": {
|
||||
"Code": {
|
||||
"CLI": "aws kinesis increase-stream-retention-period --stream-name <example_resource_name> --retention-period-hours 168",
|
||||
"NativeIaC": "```yaml\n# CloudFormation: set Kinesis stream retention to minimum required hours\nResources:\n <example_resource_name>:\n Type: AWS::Kinesis::Stream\n Properties:\n ShardCount: 1\n RetentionPeriodHours: 168 # critical: sets retention to >= 168 hours to pass the check\n```",
|
||||
"Other": "1. Sign in to the AWS Console and open Amazon Kinesis\n2. Go to Data streams and select <example_resource_name>\n3. Click Edit\n4. Set Retention period to 168 hours (or higher, per your policy)\n5. Click Save changes",
|
||||
"Terraform": "```hcl\n# Kinesis stream with adequate retention period\nresource \"aws_kinesis_stream\" \"<example_resource_name>\" {\n name = \"<example_resource_name>\"\n shard_count = 1\n retention_period = 168 # critical: sets retention to >= 168 hours to pass the check\n}\n```"
|
||||
"CLI": "aws kinesis increase-stream-retention-period --stream-name <stream-name> --retention-period-hours <hours>",
|
||||
"NativeIaC": "",
|
||||
"Other": "https://docs.aws.amazon.com/securityhub/latest/userguide/kinesis-controls.html#kinesis-3",
|
||||
"Terraform": ""
|
||||
},
|
||||
"Recommendation": {
|
||||
"Text": "Set the **retention period** to exceed worst-case consumer lag, replay needs, and compliance windows; use at least `168` hours by default (or customize as necessary) and raise as required. Enforce **change control** and least privilege on retention changes, monitor consumer lag, and maintain **secondary durability** (e.g., archival) for critical streams.",
|
||||
"Url": "https://hub.prowler.com/check/kinesis_stream_data_retention_period"
|
||||
"Text": "Configure an adequate data retention period for Kinesis streams to ensure data is available for the required timeframe. Set the retention period based on your application’s data retention requirements, and consider at least 168 hours (or customize as necessary).",
|
||||
"Url": "https://docs.aws.amazon.com/streams/latest/dev/kinesis-extended-retention.html"
|
||||
}
|
||||
},
|
||||
"Categories": [
|
||||
"resilience"
|
||||
],
|
||||
"Categories": [],
|
||||
"DependsOn": [],
|
||||
"RelatedTo": [],
|
||||
"Notes": ""
|
||||
|
||||
@@ -1,40 +1,31 @@
|
||||
{
|
||||
"Provider": "aws",
|
||||
"CheckID": "kinesis_stream_encrypted_at_rest",
|
||||
"CheckTitle": "Kinesis stream is encrypted at rest with KMS",
|
||||
"CheckTitle": "Kinesis streams should be encrypted at rest.",
|
||||
"CheckType": [
|
||||
"Software and Configuration Checks/AWS Security Best Practices",
|
||||
"Software and Configuration Checks/Industry and Regulatory Standards/AWS Foundational Security Best Practices",
|
||||
"Software and Configuration Checks/Industry and Regulatory Standards/NIST 800-53 Controls"
|
||||
],
|
||||
"ServiceName": "kinesis",
|
||||
"SubServiceName": "",
|
||||
"ResourceIdTemplate": "",
|
||||
"Severity": "high",
|
||||
"ResourceIdTemplate": "arn:partition:kinesis::account-id:stream/stream-name",
|
||||
"Severity": "medium",
|
||||
"ResourceType": "AwsKinesisStream",
|
||||
"Description": "**Amazon Kinesis Data Streams** with **server-side encryption** use **AWS KMS** to protect records at rest. The evaluation determines whether a stream has `SSE-KMS` configured with a KMS key; streams lacking KMS-based at rest encryption are identified.",
|
||||
"Risk": "Without **SSE-KMS**, records in shards may be exposed in plaintext if storage, backups, or analytics exports are accessed, undermining **confidentiality**. Absence of KMS controls also reduces **integrity** and oversight by removing key policies, rotation, and audit trails-enabling covert data exfiltration or insider misuse.",
|
||||
"RelatedUrl": "",
|
||||
"AdditionalURLs": [
|
||||
"https://docs.aws.amazon.com/securityhub/latest/userguide/kinesis-controls.html#kinesis-1",
|
||||
"https://docs.aws.amazon.com/streams/latest/dev/getting-started-with-sse.html",
|
||||
"https://www.trendmicro.com/cloudoneconformity/knowledge-base/aws/Kinesis/server-side-encryption.html"
|
||||
],
|
||||
"Description": "Ensure Kinesis streams use server-side encryption with AWS KMS keys for data protection.",
|
||||
"Risk": "If Kinesis streams are not encrypted at rest, sensitive data stored in the stream could be exposed to unauthorized access or breaches. This could lead to potential data theft or misuse of unencrypted data.",
|
||||
"RelatedUrl": "https://docs.aws.amazon.com/AmazonS3/latest/userguide/UsingKMSEncryption.html",
|
||||
"Remediation": {
|
||||
"Code": {
|
||||
"CLI": "aws kinesis start-stream-encryption --stream-name <KINESIS_STREAM_NAME> --encryption-type KMS --key-id alias/aws/kinesis",
|
||||
"NativeIaC": "```yaml\n# CloudFormation: enable KMS encryption on a Kinesis stream\nResources:\n <example_resource_name>:\n Type: AWS::Kinesis::Stream\n Properties:\n ShardCount: 1\n StreamEncryption:\n EncryptionType: KMS # Critical: enables KMS encryption at rest\n KeyId: alias/aws/kinesis # Critical: uses AWS managed Kinesis KMS key\n```",
|
||||
"Other": "1. Open the AWS Console and go to Amazon Kinesis > Data streams\n2. Select the stream\n3. On the Details tab, click Edit in Server-side encryption\n4. Select Enabled\n5. Choose the (Default) aws/kinesis KMS key\n6. Click Save",
|
||||
"Terraform": "```hcl\n# Enable KMS encryption on a Kinesis stream\nresource \"aws_kinesis_stream\" \"<example_resource_name>\" {\n name = \"<example_resource_name>\"\n shard_count = 1\n encryption_type = \"KMS\" # Critical: enables KMS encryption at rest\n kms_key_id = \"alias/aws/kinesis\" # Critical: uses AWS managed Kinesis KMS key\n}\n```"
|
||||
"CLI": "aws kinesis start-stream-encryption --stream-name <your-stream-name> --encryption-type KMS --key-id <your-kms-key-id>",
|
||||
"NativeIaC": "https://docs.prowler.com/checks/aws/general-policies/bc_aws_general_22/#cloudformation",
|
||||
"Other": "https://docs.aws.amazon.com/securityhub/latest/userguide/kinesis-controls.html#kinesis-1",
|
||||
"Terraform": "https://docs.prowler.com/checks/aws/general-policies/bc_aws_general_22/#terraform"
|
||||
},
|
||||
"Recommendation": {
|
||||
"Text": "Enable **SSE-KMS** on all streams.\n- Use **customer-managed keys** for rotation and ownership\n- Enforce **least privilege** on KMS grants; limit cross-account use\n- Monitor key usage and require encryption in CI/CD",
|
||||
"Url": "https://hub.prowler.com/check/kinesis_stream_encrypted_at_rest"
|
||||
"Text": "Enable server-side encryption for Kinesis streams using AWS KMS keys to ensure that all data is encrypted before it is stored, protecting data at rest and reducing the risk of unauthorized access.",
|
||||
"Url": "https://docs.aws.amazon.com/streams/latest/dev/getting-started-with-sse.html"
|
||||
}
|
||||
},
|
||||
"Categories": [
|
||||
"encryption"
|
||||
],
|
||||
"Categories": [],
|
||||
"DependsOn": [],
|
||||
"RelatedTo": [],
|
||||
"Notes": ""
|
||||
|
||||
@@ -25,25 +25,10 @@ class CloudResourceManager(GCPService):
|
||||
.execute(num_retries=DEFAULT_RETRY_ATTEMPTS)
|
||||
)
|
||||
audit_logging = False
|
||||
audit_configs = []
|
||||
if policy.get("auditConfigs"):
|
||||
audit_logging = True
|
||||
for config in policy.get("auditConfigs", []):
|
||||
log_types = []
|
||||
for log_config in config.get("auditLogConfigs", []):
|
||||
log_types.append(log_config.get("logType", ""))
|
||||
audit_configs.append(
|
||||
AuditConfig(
|
||||
service=config.get("service", ""),
|
||||
log_types=log_types,
|
||||
)
|
||||
)
|
||||
self.cloud_resource_manager_projects.append(
|
||||
Project(
|
||||
id=project_id,
|
||||
audit_logging=audit_logging,
|
||||
audit_configs=audit_configs,
|
||||
)
|
||||
Project(id=project_id, audit_logging=audit_logging)
|
||||
)
|
||||
for binding in policy["bindings"]:
|
||||
self.bindings.append(
|
||||
@@ -55,9 +40,7 @@ class CloudResourceManager(GCPService):
|
||||
)
|
||||
except Exception as error:
|
||||
logger.error(
|
||||
f"{self.region} -- "
|
||||
f"{error.__class__.__name__}"
|
||||
f"[{error.__traceback__.tb_lineno}]: {error}"
|
||||
f"{self.region} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
|
||||
)
|
||||
|
||||
def _get_organizations(self):
|
||||
@@ -71,23 +54,15 @@ class CloudResourceManager(GCPService):
|
||||
for org in response.get("organizations", []):
|
||||
self.organizations.append(
|
||||
Organization(
|
||||
id=org["name"].split("/")[-1],
|
||||
name=org["displayName"],
|
||||
id=org["name"].split("/")[-1], name=org["displayName"]
|
||||
)
|
||||
)
|
||||
except Exception as error:
|
||||
logger.error(
|
||||
f"{self.region} -- "
|
||||
f"{error.__class__.__name__}"
|
||||
f"[{error.__traceback__.tb_lineno}]: {error}"
|
||||
f"{self.region} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
|
||||
)
|
||||
|
||||
|
||||
class AuditConfig(BaseModel):
|
||||
service: str
|
||||
log_types: list[str]
|
||||
|
||||
|
||||
class Binding(BaseModel):
|
||||
role: str
|
||||
members: list
|
||||
@@ -97,7 +72,6 @@ class Binding(BaseModel):
|
||||
class Project(BaseModel):
|
||||
id: str
|
||||
audit_logging: bool
|
||||
audit_configs: list[AuditConfig] = []
|
||||
|
||||
|
||||
class Organization(BaseModel):
|
||||
|
||||
@@ -1,36 +0,0 @@
|
||||
{
|
||||
"Provider": "gcp",
|
||||
"CheckID": "cloudstorage_audit_logs_enabled",
|
||||
"CheckTitle": "Data Access audit logs are enabled for Cloud Storage",
|
||||
"CheckType": [],
|
||||
"ServiceName": "cloudstorage",
|
||||
"SubServiceName": "",
|
||||
"ResourceIdTemplate": "",
|
||||
"Severity": "medium",
|
||||
"ResourceType": "cloudresourcemanager.googleapis.com/Project",
|
||||
"Description": "Data Access audit logs (DATA_READ and DATA_WRITE) are enabled for Cloud Storage at the project level. Unlike Admin Activity logs (enabled by default), Data Access logs must be explicitly configured to track read and write operations on Cloud Storage objects.",
|
||||
"Risk": "Without Data Access audit logs, you cannot track who accessed or modified objects in your Cloud Storage buckets, making it difficult to detect unauthorized access, data exfiltration, or compliance violations.",
|
||||
"RelatedUrl": "",
|
||||
"AdditionalURLs": [
|
||||
"https://www.trendmicro.com/cloudoneconformity/knowledge-base/gcp/CloudStorage/enable-data-access-audit-logs.html",
|
||||
"https://cloud.google.com/storage/docs/audit-logging"
|
||||
],
|
||||
"Remediation": {
|
||||
"Code": {
|
||||
"CLI": "",
|
||||
"NativeIaC": "",
|
||||
"Other": "1) Console → IAM & Admin → Audit Logs\n2) Find 'Google Cloud Storage' in the list of services\n3) Check the boxes for 'Data Read' and 'Data Write'\n4) Click 'Save' to apply the configuration\n\nNote: This is a project-level setting that applies to all Cloud Storage buckets in the project.",
|
||||
"Terraform": "```hcl\nresource \"google_project_iam_audit_config\" \"storage_audit\" {\n project = var.project_id\n service = \"storage.googleapis.com\"\n\n audit_log_config {\n log_type = \"DATA_READ\"\n }\n\n audit_log_config {\n log_type = \"DATA_WRITE\"\n }\n}\n```"
|
||||
},
|
||||
"Recommendation": {
|
||||
"Text": "Enable Data Access audit logs (DATA_READ and DATA_WRITE) for Cloud Storage at the project level to track all read and write operations on storage objects for security monitoring and compliance.",
|
||||
"Url": "https://hub.prowler.com/check/cloudstorage_audit_logs_enabled"
|
||||
}
|
||||
},
|
||||
"Categories": [
|
||||
"logging"
|
||||
],
|
||||
"DependsOn": [],
|
||||
"RelatedTo": [],
|
||||
"Notes": ""
|
||||
}
|
||||
@@ -1,61 +0,0 @@
|
||||
from prowler.lib.check.models import Check, Check_Report_GCP
|
||||
from prowler.providers.gcp.services.cloudresourcemanager.cloudresourcemanager_client import (
|
||||
cloudresourcemanager_client,
|
||||
)
|
||||
|
||||
|
||||
class cloudstorage_audit_logs_enabled(Check):
|
||||
"""
|
||||
Ensure GCP Cloud Storage data access audit logs are enabled.
|
||||
|
||||
- PASS: Project has audit config for storage.googleapis.com or allServices with
|
||||
DATA_READ and DATA_WRITE log types enabled.
|
||||
- FAIL: Project is missing audit config for Cloud Storage,
|
||||
or missing DATA_READ or DATA_WRITE log types.
|
||||
"""
|
||||
|
||||
def execute(self) -> list[Check_Report_GCP]:
|
||||
findings = []
|
||||
|
||||
for project in cloudresourcemanager_client.cloud_resource_manager_projects:
|
||||
report = Check_Report_GCP(
|
||||
metadata=self.metadata(),
|
||||
resource=cloudresourcemanager_client.projects[project.id],
|
||||
project_id=project.id,
|
||||
location=cloudresourcemanager_client.region,
|
||||
resource_name=(
|
||||
cloudresourcemanager_client.projects[project.id].name
|
||||
if cloudresourcemanager_client.projects[project.id].name
|
||||
else "GCP Project"
|
||||
),
|
||||
)
|
||||
|
||||
log_types_set = set()
|
||||
for config in project.audit_configs:
|
||||
if config.service in ["storage.googleapis.com", "allServices"]:
|
||||
log_types_set.update(config.log_types)
|
||||
|
||||
required_logs = {"DATA_READ", "DATA_WRITE"}
|
||||
|
||||
if project.audit_logging:
|
||||
if required_logs.issubset(log_types_set):
|
||||
report.status = "PASS"
|
||||
report.status_extended = f"Project {project.id} has Data Access audit logs (DATA_READ and DATA_WRITE) enabled for Cloud Storage."
|
||||
else:
|
||||
report.status = "FAIL"
|
||||
if not log_types_set:
|
||||
report.status_extended = f"Project {project.id} has Audit Logs enabled for other services but not for Cloud Storage."
|
||||
else:
|
||||
report.status_extended = (
|
||||
f"Project {project.id} has Audit Logs enabled for Cloud Storage but is missing some required log types"
|
||||
f"(missing: {', '.join(sorted(required_logs - log_types_set))})."
|
||||
)
|
||||
else:
|
||||
report.status = "FAIL"
|
||||
report.status_extended = (
|
||||
f"Project {project.id} does not have Audit Logs enabled."
|
||||
)
|
||||
|
||||
findings.append(report)
|
||||
|
||||
return findings
|
||||
@@ -45,12 +45,11 @@ class IacProvider(Provider):
|
||||
self.scan_repository_url = scan_repository_url
|
||||
self.scanners = scanners
|
||||
self.exclude_path = exclude_path
|
||||
self.region = "branch"
|
||||
self.region = "global"
|
||||
self.audited_account = "local-iac"
|
||||
self._session = None
|
||||
self._identity = "prowler"
|
||||
self._auth_method = "No auth"
|
||||
self._temp_clone_dir = None # Track temporary directory for cleanup
|
||||
|
||||
if scan_repository_url:
|
||||
oauth_app_token = oauth_app_token or environ.get("GITHUB_OAUTH_APP_TOKEN")
|
||||
@@ -81,20 +80,6 @@ class IacProvider(Provider):
|
||||
"No GitHub authentication method provided; proceeding without authentication."
|
||||
)
|
||||
|
||||
# Clone repository and detect branch during initialization
|
||||
# This ensures the branch is detected for both CLI and API usage
|
||||
self._temp_clone_dir, branch_name = self._clone_repository(
|
||||
self.scan_repository_url,
|
||||
self.github_username,
|
||||
self.personal_access_token,
|
||||
self.oauth_app_token,
|
||||
)
|
||||
# Update scan_path to point to the cloned repository
|
||||
self.scan_path = self._temp_clone_dir
|
||||
# Update region with the detected branch name
|
||||
self.region = branch_name
|
||||
logger.info(f"Updated region to branch: {branch_name}")
|
||||
|
||||
# Audit Config
|
||||
if config_content:
|
||||
self._audit_config = config_content
|
||||
@@ -146,20 +131,6 @@ class IacProvider(Provider):
|
||||
def fixer_config(self):
|
||||
return self._fixer_config
|
||||
|
||||
def __del__(self):
|
||||
"""Cleanup temporary directory when provider is destroyed"""
|
||||
self.cleanup()
|
||||
|
||||
def cleanup(self):
|
||||
"""Remove temporary cloned repository if it exists"""
|
||||
if self._temp_clone_dir:
|
||||
try:
|
||||
logger.info(f"Removing temporary directory {self._temp_clone_dir}...")
|
||||
shutil.rmtree(self._temp_clone_dir)
|
||||
self._temp_clone_dir = None
|
||||
except Exception as error:
|
||||
logger.warning(f"Failed to remove temporary directory: {error}")
|
||||
|
||||
def setup_session(self):
|
||||
"""IAC provider doesn't need a session since it uses Trivy directly"""
|
||||
return None
|
||||
@@ -237,8 +208,6 @@ class IacProvider(Provider):
|
||||
)
|
||||
if finding_status == "MUTED":
|
||||
report.muted = True
|
||||
# Set the region from the provider
|
||||
report.region = self.region
|
||||
return report
|
||||
except Exception as error:
|
||||
logger.critical(
|
||||
@@ -246,50 +215,15 @@ class IacProvider(Provider):
|
||||
)
|
||||
sys.exit(1)
|
||||
|
||||
def _detect_branch_name(self, repo_path: str) -> str:
|
||||
"""
|
||||
Detect the current branch name from a cloned repository.
|
||||
|
||||
Args:
|
||||
repo_path: Path to the cloned repository
|
||||
|
||||
Returns:
|
||||
str: The branch name, defaulting to "main" if detection fails
|
||||
"""
|
||||
try:
|
||||
import os
|
||||
|
||||
# Read .git/HEAD to detect the current branch
|
||||
head_file = os.path.join(repo_path, ".git", "HEAD")
|
||||
if os.path.exists(head_file):
|
||||
with open(head_file, "r") as f:
|
||||
content = f.read().strip()
|
||||
# Format: "ref: refs/heads/branch-name"
|
||||
if content.startswith("ref: refs/heads/"):
|
||||
branch_name = content[16:] # Remove "ref: refs/heads/"
|
||||
logger.info(f"Detected branch: {branch_name}")
|
||||
return branch_name
|
||||
|
||||
# Fallback: return "main" as default
|
||||
logger.warning("Could not detect branch name, defaulting to 'main'")
|
||||
return "main"
|
||||
|
||||
except Exception as error:
|
||||
logger.error(f"Error detecting branch name: {error}")
|
||||
return "main" # Safe fallback
|
||||
|
||||
def _clone_repository(
|
||||
self,
|
||||
repository_url: str,
|
||||
github_username: str = None,
|
||||
personal_access_token: str = None,
|
||||
oauth_app_token: str = None,
|
||||
) -> tuple[str, str]:
|
||||
) -> str:
|
||||
"""
|
||||
Clone a git repository to a temporary directory, supporting GitHub authentication.
|
||||
|
||||
Returns:
|
||||
tuple[str, str]: (temporary_directory, branch_name)
|
||||
"""
|
||||
try:
|
||||
original_url = repository_url
|
||||
@@ -342,36 +276,33 @@ class IacProvider(Provider):
|
||||
porcelain.clone(repository_url, temporary_directory, depth=1)
|
||||
logger.info("Repository cloned successfully!")
|
||||
|
||||
# Detect the branch name from the cloned repository
|
||||
branch_name = self._detect_branch_name(temporary_directory)
|
||||
|
||||
return temporary_directory, branch_name
|
||||
return temporary_directory
|
||||
except Exception as error:
|
||||
logger.critical(
|
||||
f"{error.__class__.__name__}:{error.__traceback__.tb_lineno} -- {error}"
|
||||
)
|
||||
|
||||
def run(self) -> List[CheckReportIAC]:
|
||||
"""
|
||||
Execute the IaC scan.
|
||||
temp_dir = None
|
||||
if self.scan_repository_url:
|
||||
scan_dir = temp_dir = self._clone_repository(
|
||||
self.scan_repository_url,
|
||||
getattr(self, "github_username", None),
|
||||
getattr(self, "personal_access_token", None),
|
||||
getattr(self, "oauth_app_token", None),
|
||||
)
|
||||
else:
|
||||
scan_dir = self.scan_path
|
||||
|
||||
Note: Repository cloning and branch detection now happen in __init__(),
|
||||
so this method just runs the scan and returns results.
|
||||
For CLI compatibility, cleanup is still performed at the end.
|
||||
"""
|
||||
try:
|
||||
# Collect all batches from the generator
|
||||
# scan_path now points to either the local directory or the cloned repo
|
||||
reports = []
|
||||
for batch in self.run_scan(
|
||||
self.scan_path, self.scanners, self.exclude_path
|
||||
):
|
||||
for batch in self.run_scan(scan_dir, self.scanners, self.exclude_path):
|
||||
reports.extend(batch)
|
||||
finally:
|
||||
# Clean up temporary directory if this was a repository scan
|
||||
# This ensures CLI usage cleans up immediately after run()
|
||||
if self._temp_clone_dir:
|
||||
self.cleanup()
|
||||
if temp_dir:
|
||||
logger.info(f"Removing temporary directory {temp_dir}...")
|
||||
shutil.rmtree(temp_dir)
|
||||
|
||||
return reports
|
||||
|
||||
|
||||
@@ -13,28 +13,32 @@ class exchange_mailbox_policy_additional_storage_restricted(Check):
|
||||
def execute(self) -> List[CheckReportM365]:
|
||||
"""Run the check to validate Exchange mailbox policy restrictions.
|
||||
|
||||
Iterates through all mailbox policies to determine if additional storage
|
||||
providers are restricted and generates reports for each policy.
|
||||
Iterates through the mailbox policy configuration to determine if additional storage
|
||||
providers are restricted and generates a report based on the policy status.
|
||||
|
||||
Returns:
|
||||
List[CheckReportM365]: A list of reports with the restriction status for each mailbox policy.
|
||||
List[CheckReportM365]: A list of reports with the restriction status for the mailbox policy.
|
||||
"""
|
||||
findings = []
|
||||
for mailbox_policy in exchange_client.mailbox_policies:
|
||||
if mailbox_policy:
|
||||
report = CheckReportM365(
|
||||
metadata=self.metadata(),
|
||||
resource=mailbox_policy,
|
||||
resource_name=f"Exchange Mailbox Policy - {mailbox_policy.id}",
|
||||
resource_id=mailbox_policy.id,
|
||||
mailbox_policy = exchange_client.mailbox_policy
|
||||
if mailbox_policy:
|
||||
report = CheckReportM365(
|
||||
metadata=self.metadata(),
|
||||
resource=mailbox_policy,
|
||||
resource_name="Exchange Mailbox Policy",
|
||||
resource_id=mailbox_policy.id,
|
||||
)
|
||||
report.status = "FAIL"
|
||||
report.status_extended = (
|
||||
"Exchange mailbox policy allows additional storage providers."
|
||||
)
|
||||
|
||||
if not mailbox_policy.additional_storage_enabled:
|
||||
report.status = "PASS"
|
||||
report.status_extended = (
|
||||
"Exchange mailbox policy restricts additional storage providers."
|
||||
)
|
||||
report.status = "FAIL"
|
||||
report.status_extended = f"Exchange mailbox policy '{mailbox_policy.id}' allows additional storage providers."
|
||||
|
||||
if not mailbox_policy.additional_storage_enabled:
|
||||
report.status = "PASS"
|
||||
report.status_extended = f"Exchange mailbox policy '{mailbox_policy.id}' restricts additional storage providers."
|
||||
|
||||
findings.append(report)
|
||||
findings.append(report)
|
||||
|
||||
return findings
|
||||
|
||||
@@ -16,7 +16,7 @@ class Exchange(M365Service):
|
||||
self.external_mail_config = []
|
||||
self.transport_rules = []
|
||||
self.transport_config = None
|
||||
self.mailbox_policies = []
|
||||
self.mailbox_policy = None
|
||||
self.role_assignment_policies = []
|
||||
self.mailbox_audit_properties = []
|
||||
|
||||
@@ -27,7 +27,7 @@ class Exchange(M365Service):
|
||||
self.external_mail_config = self._get_external_mail_config()
|
||||
self.transport_rules = self._get_transport_rules()
|
||||
self.transport_config = self._get_transport_config()
|
||||
self.mailbox_policies = self._get_mailbox_policy()
|
||||
self.mailbox_policy = self._get_mailbox_policy()
|
||||
self.role_assignment_policies = self._get_role_assignment_policies()
|
||||
self.mailbox_audit_properties = self._get_mailbox_audit_properties()
|
||||
self.powershell.close()
|
||||
@@ -164,27 +164,21 @@ class Exchange(M365Service):
|
||||
|
||||
def _get_mailbox_policy(self):
|
||||
logger.info("Microsoft365 - Getting mailbox policy configuration...")
|
||||
mailbox_policies = []
|
||||
mailboxes_policy = None
|
||||
try:
|
||||
policies_data = self.powershell.get_mailbox_policy()
|
||||
if policies_data:
|
||||
if isinstance(policies_data, dict):
|
||||
policies_data = [policies_data]
|
||||
for policy in policies_data:
|
||||
if policy:
|
||||
mailbox_policies.append(
|
||||
MailboxPolicy(
|
||||
id=policy.get("Id", ""),
|
||||
additional_storage_enabled=policy.get(
|
||||
"AdditionalStorageProvidersAvailable", True
|
||||
),
|
||||
)
|
||||
)
|
||||
mailbox_policy = self.powershell.get_mailbox_policy()
|
||||
if mailbox_policy:
|
||||
mailboxes_policy = MailboxPolicy(
|
||||
id=mailbox_policy.get("Id", ""),
|
||||
additional_storage_enabled=mailbox_policy.get(
|
||||
"AdditionalStorageProvidersAvailable", True
|
||||
),
|
||||
)
|
||||
except Exception as error:
|
||||
logger.error(
|
||||
f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
|
||||
)
|
||||
return mailbox_policies
|
||||
return mailboxes_policy
|
||||
|
||||
def _get_role_assignment_policies(self):
|
||||
logger.info("Microsoft365 - Getting role assignment policies...")
|
||||
|
||||
@@ -11,9 +11,12 @@ class sharepoint_external_sharing_managed(Check):
|
||||
Check if Microsoft 365 SharePoint external sharing is managed through domain whitelists/blacklists.
|
||||
|
||||
This check verifies that SharePoint external sharing settings are configured to restrict document sharing
|
||||
to external domains by enforcing domain-based restrictions. When external sharing is enabled, the setting
|
||||
'sharingDomainRestrictionMode' must be set to either "AllowList" or "BlockList" with a corresponding
|
||||
domain list. If external sharing is disabled at the organization level, the check passes.
|
||||
to external domains by enforcing domain-based restrictions. This means that the setting
|
||||
'sharingDomainRestrictionMode' must be set to either "AllowList" or "BlockList". If it is not, then
|
||||
external sharing is not managed via domain restrictions, increasing the risk of unauthorized access.
|
||||
|
||||
Note: This check only evaluates the domain restriction mode and does not enforce the optional check
|
||||
of verifying that the allowed/blocked domain list is not empty.
|
||||
"""
|
||||
|
||||
def execute(self) -> List[CheckReportM365]:
|
||||
@@ -37,12 +40,7 @@ class sharepoint_external_sharing_managed(Check):
|
||||
)
|
||||
report.status = "FAIL"
|
||||
report.status_extended = "SharePoint external sharing is not managed through domain restrictions."
|
||||
if settings.sharingCapability == "Disabled":
|
||||
report.status = "PASS"
|
||||
report.status_extended = (
|
||||
"External sharing is disabled at organization level."
|
||||
)
|
||||
elif settings.sharingDomainRestrictionMode in ["allowList", "blockList"]:
|
||||
if settings.sharingDomainRestrictionMode in ["allowList", "blockList"]:
|
||||
report.status_extended = f"SharePoint external sharing is managed through domain restrictions with mode '{settings.sharingDomainRestrictionMode}' but the list is empty."
|
||||
if (
|
||||
settings.sharingDomainRestrictionMode == "allowList"
|
||||
|
||||
@@ -77,7 +77,7 @@ maintainers = [{name = "Prowler Engineering", email = "engineering@prowler.com"}
|
||||
name = "prowler"
|
||||
readme = "README.md"
|
||||
requires-python = ">3.9.1,<3.13"
|
||||
version = "5.14.3"
|
||||
version = "5.14.0"
|
||||
|
||||
[project.scripts]
|
||||
prowler = "prowler.__main__:prowler"
|
||||
|
||||
@@ -1,61 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Setup Git Hooks for Prowler
|
||||
# This script installs pre-commit hooks using the project's Poetry environment
|
||||
|
||||
set -e
|
||||
|
||||
# Colors
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
RED='\033[0;31m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
echo ""
|
||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
echo "🔧 Setting up Prowler Git Hooks"
|
||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
echo ""
|
||||
|
||||
# Check if we're in a git repository
|
||||
if ! git rev-parse --git-dir > /dev/null 2>&1; then
|
||||
echo -e "${RED}❌ Not in a git repository${NC}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check if Poetry is installed
|
||||
if ! command -v poetry &> /dev/null; then
|
||||
echo -e "${RED}❌ Poetry is not installed${NC}"
|
||||
echo -e "${YELLOW} Install Poetry: https://python-poetry.org/docs/#installation${NC}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check if pyproject.toml exists
|
||||
if [ ! -f "pyproject.toml" ]; then
|
||||
echo -e "${RED}❌ pyproject.toml not found${NC}"
|
||||
echo -e "${YELLOW} Please run this script from the repository root${NC}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check if dependencies are already installed
|
||||
if ! poetry run python -c "import pre_commit" 2>/dev/null; then
|
||||
echo -e "${YELLOW}📦 Installing project dependencies (including pre-commit)...${NC}"
|
||||
poetry install --with dev
|
||||
else
|
||||
echo -e "${GREEN}✓${NC} Dependencies already installed"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo -e "${YELLOW}🔗 Installing pre-commit hooks...${NC}"
|
||||
poetry run pre-commit install
|
||||
|
||||
echo ""
|
||||
echo -e "${GREEN}✅ Git hooks successfully configured!${NC}"
|
||||
echo ""
|
||||
echo -e "${YELLOW}📋 Pre-commit system:${NC}"
|
||||
echo -e " • Python pre-commit manages all git hooks"
|
||||
echo -e " • API files: Python checks (black, flake8, bandit, etc.)"
|
||||
echo -e " • UI files: UI checks (TypeScript, ESLint, Claude Code validation)"
|
||||
echo ""
|
||||
echo -e "${GREEN}🎉 Setup complete!${NC}"
|
||||
echo ""
|
||||
@@ -18,7 +18,6 @@ from prowler.lib.check.check import (
|
||||
list_categories,
|
||||
list_checks_json,
|
||||
list_services,
|
||||
load_custom_checks_metadata,
|
||||
parse_checks_from_file,
|
||||
parse_checks_from_folder,
|
||||
remove_custom_checks_module,
|
||||
@@ -484,49 +483,6 @@ class TestCheck:
|
||||
)
|
||||
remove_custom_checks_module(check_folder, provider)
|
||||
|
||||
def test_load_custom_checks_metadata(self, tmp_path):
|
||||
"""Test loading check metadata from a custom checks folder."""
|
||||
check_name = "custom_test_check"
|
||||
check_folder = tmp_path / check_name
|
||||
check_folder.mkdir()
|
||||
|
||||
metadata = {
|
||||
"Provider": "aws",
|
||||
"CheckID": check_name,
|
||||
"CheckTitle": "Test Custom Check",
|
||||
"CheckType": [],
|
||||
"ServiceName": "custom",
|
||||
"SubServiceName": "",
|
||||
"ResourceIdTemplate": "arn:aws:custom:::resource",
|
||||
"Severity": "low",
|
||||
"ResourceType": "AwsCustomResource",
|
||||
"Description": "A test custom check",
|
||||
"Risk": "Test risk",
|
||||
"RelatedUrl": "https://example.com",
|
||||
"Remediation": {
|
||||
"Code": {"CLI": "", "NativeIaC": "", "Other": "", "Terraform": ""},
|
||||
"Recommendation": {"Text": "", "Url": ""},
|
||||
},
|
||||
"Categories": [],
|
||||
"DependsOn": [],
|
||||
"RelatedTo": [],
|
||||
"Notes": "",
|
||||
}
|
||||
metadata_file = check_folder / f"{check_name}.metadata.json"
|
||||
metadata_file.write_text(json.dumps(metadata))
|
||||
|
||||
result = load_custom_checks_metadata(str(tmp_path))
|
||||
|
||||
assert check_name in result
|
||||
assert result[check_name].CheckID == check_name
|
||||
assert result[check_name].Provider == "aws"
|
||||
assert result[check_name].Severity == "low"
|
||||
|
||||
def test_load_custom_checks_metadata_nonexistent_path(self):
|
||||
"""Test that nonexistent paths return empty dict."""
|
||||
result = load_custom_checks_metadata("/nonexistent/path/to/checks")
|
||||
assert result == {}
|
||||
|
||||
def test_exclude_checks_to_run(self):
|
||||
test_cases = [
|
||||
{
|
||||
|
||||
@@ -662,7 +662,6 @@ class TestFinding:
|
||||
check_output.resource_name = "aws_s3_bucket.example"
|
||||
check_output.resource_path = "/path/to/iac/file.tf"
|
||||
check_output.resource_line_range = "1:5"
|
||||
check_output.region = "main" # Branch name for remote IaC scans
|
||||
check_output.resource = {
|
||||
"resource": "aws_s3_bucket.example",
|
||||
"value": {},
|
||||
@@ -686,7 +685,7 @@ class TestFinding:
|
||||
assert finding_output.auth_method == "No auth"
|
||||
assert finding_output.resource_name == "aws_s3_bucket.example"
|
||||
assert finding_output.resource_uid == "aws_s3_bucket.example"
|
||||
assert finding_output.region == "main" # Branch name, not line range
|
||||
assert finding_output.region == "1:5"
|
||||
assert finding_output.status == Status.PASS
|
||||
assert finding_output.status_extended == "mock_status_extended"
|
||||
assert finding_output.muted is False
|
||||
|
||||
@@ -398,7 +398,7 @@ def get_aws_html_header(args: list) -> str:
|
||||
<div class="row mt-3">
|
||||
<div class="col-md-4">
|
||||
<a href="https://github.com/prowler-cloud/prowler/"><img class="float-left card-img-left mt-4 mr-4 ml-4"
|
||||
src=https://raw.githubusercontent.com/prowler-cloud/prowler/dc7d2d5aeb92fdf12e8604f42ef6472cd3e8e889/docs/img/prowler-logo-black.png
|
||||
src=https://prowler.com/wp-content/uploads/logo-html.png
|
||||
alt="prowler-logo"
|
||||
style="width: 15rem; height:auto;"/></a>
|
||||
<div class="card">
|
||||
|
||||
@@ -85,15 +85,7 @@ def mock_api_projects_calls(client: MagicMock):
|
||||
client.projects().locations().keys().list_next.return_value = None
|
||||
# Mocking policy
|
||||
client.projects().getIamPolicy().execute.return_value = {
|
||||
"auditConfigs": [
|
||||
{
|
||||
"service": "allServices",
|
||||
"auditLogConfigs": [
|
||||
{"logType": "ADMIN_READ"},
|
||||
{"logType": "DATA_WRITE"},
|
||||
],
|
||||
}
|
||||
],
|
||||
"auditConfigs": [MagicMock()],
|
||||
"bindings": [
|
||||
{
|
||||
"role": "roles/resourcemanager.organizationAdmin",
|
||||
@@ -254,15 +246,7 @@ def mock_api_projects_calls(client: MagicMock):
|
||||
== "projects/123/locations/eu-west1/keyRings/keyring1/cryptoKeys/key1"
|
||||
):
|
||||
return_value.execute.return_value = {
|
||||
"auditConfigs": [
|
||||
{
|
||||
"service": "allServices",
|
||||
"auditLogConfigs": [
|
||||
{"logType": "ADMIN_READ"},
|
||||
{"logType": "DATA_WRITE"},
|
||||
],
|
||||
}
|
||||
],
|
||||
"auditConfigs": [MagicMock()],
|
||||
"bindings": [
|
||||
{
|
||||
"role": "roles/resourcemanager.organizationAdmin",
|
||||
@@ -290,15 +274,7 @@ def mock_api_projects_calls(client: MagicMock):
|
||||
== "projects/123/locations/eu-west1/keyRings/keyring2/cryptoKeys/key2"
|
||||
):
|
||||
return_value.execute.return_value = {
|
||||
"auditConfigs": [
|
||||
{
|
||||
"service": "allServices",
|
||||
"auditLogConfigs": [
|
||||
{"logType": "ADMIN_READ"},
|
||||
{"logType": "DATA_WRITE"},
|
||||
],
|
||||
}
|
||||
],
|
||||
"auditConfigs": [MagicMock()],
|
||||
"bindings": [
|
||||
{
|
||||
"role": "roles/resourcemanager.organizationAdmin",
|
||||
|
||||
@@ -1,398 +0,0 @@
|
||||
from unittest import mock
|
||||
|
||||
from prowler.providers.gcp.models import GCPProject
|
||||
from tests.providers.gcp.gcp_fixtures import (
|
||||
GCP_PROJECT_ID,
|
||||
GCP_US_CENTER1_LOCATION,
|
||||
set_mocked_gcp_provider,
|
||||
)
|
||||
|
||||
|
||||
class TestCloudStorageAuditLogsEnabled:
|
||||
def test_no_projects(self):
|
||||
cloudresourcemanager_client = mock.MagicMock()
|
||||
|
||||
with (
|
||||
mock.patch(
|
||||
"prowler.providers.common.provider.Provider.get_global_provider",
|
||||
return_value=set_mocked_gcp_provider(),
|
||||
),
|
||||
mock.patch(
|
||||
"prowler.providers.gcp.services.cloudstorage."
|
||||
"cloudstorage_audit_logs_enabled."
|
||||
"cloudstorage_audit_logs_enabled."
|
||||
"cloudresourcemanager_client",
|
||||
new=cloudresourcemanager_client,
|
||||
),
|
||||
):
|
||||
from prowler.providers.gcp.services.cloudstorage.cloudstorage_audit_logs_enabled.cloudstorage_audit_logs_enabled import (
|
||||
cloudstorage_audit_logs_enabled,
|
||||
)
|
||||
|
||||
cloudresourcemanager_client.cloud_resource_manager_projects = []
|
||||
cloudresourcemanager_client.region = GCP_US_CENTER1_LOCATION
|
||||
|
||||
check = cloudstorage_audit_logs_enabled()
|
||||
result = check.execute()
|
||||
|
||||
assert len(result) == 0
|
||||
|
||||
def test_project_with_storage_audit_logs_enabled(self):
|
||||
cloudresourcemanager_client = mock.MagicMock()
|
||||
|
||||
with (
|
||||
mock.patch(
|
||||
"prowler.providers.common.provider.Provider.get_global_provider",
|
||||
return_value=set_mocked_gcp_provider(),
|
||||
),
|
||||
mock.patch(
|
||||
"prowler.providers.gcp.services.cloudstorage."
|
||||
"cloudstorage_audit_logs_enabled."
|
||||
"cloudstorage_audit_logs_enabled."
|
||||
"cloudresourcemanager_client",
|
||||
new=cloudresourcemanager_client,
|
||||
),
|
||||
):
|
||||
from prowler.providers.gcp.services.cloudresourcemanager.cloudresourcemanager_service import (
|
||||
AuditConfig,
|
||||
Project,
|
||||
)
|
||||
from prowler.providers.gcp.services.cloudstorage.cloudstorage_audit_logs_enabled.cloudstorage_audit_logs_enabled import (
|
||||
cloudstorage_audit_logs_enabled,
|
||||
)
|
||||
|
||||
project = Project(
|
||||
id=GCP_PROJECT_ID,
|
||||
audit_logging=True,
|
||||
audit_configs=[
|
||||
AuditConfig(
|
||||
service="storage.googleapis.com",
|
||||
log_types=["DATA_READ", "DATA_WRITE"],
|
||||
)
|
||||
],
|
||||
)
|
||||
|
||||
cloudresourcemanager_client.cloud_resource_manager_projects = [project]
|
||||
cloudresourcemanager_client.region = GCP_US_CENTER1_LOCATION
|
||||
cloudresourcemanager_client.projects = {
|
||||
GCP_PROJECT_ID: GCPProject(
|
||||
id=GCP_PROJECT_ID,
|
||||
number="123456789012",
|
||||
name="test-project",
|
||||
labels={},
|
||||
lifecycle_state="ACTIVE",
|
||||
)
|
||||
}
|
||||
|
||||
check = cloudstorage_audit_logs_enabled()
|
||||
result = check.execute()
|
||||
|
||||
assert len(result) == 1
|
||||
assert result[0].status == "PASS"
|
||||
assert result[0].status_extended == (
|
||||
f"Project {GCP_PROJECT_ID} has Data Access audit logs "
|
||||
f"(DATA_READ and DATA_WRITE) enabled for Cloud Storage."
|
||||
)
|
||||
assert result[0].project_id == GCP_PROJECT_ID
|
||||
assert result[0].location == GCP_US_CENTER1_LOCATION
|
||||
assert result[0].resource_name == "test-project"
|
||||
|
||||
def test_project_with_audit_logs_but_no_storage_config(self):
|
||||
cloudresourcemanager_client = mock.MagicMock()
|
||||
|
||||
with (
|
||||
mock.patch(
|
||||
"prowler.providers.common.provider.Provider.get_global_provider",
|
||||
return_value=set_mocked_gcp_provider(),
|
||||
),
|
||||
mock.patch(
|
||||
"prowler.providers.gcp.services.cloudstorage."
|
||||
"cloudstorage_audit_logs_enabled."
|
||||
"cloudstorage_audit_logs_enabled."
|
||||
"cloudresourcemanager_client",
|
||||
new=cloudresourcemanager_client,
|
||||
),
|
||||
):
|
||||
from prowler.providers.gcp.services.cloudresourcemanager.cloudresourcemanager_service import (
|
||||
AuditConfig,
|
||||
Project,
|
||||
)
|
||||
from prowler.providers.gcp.services.cloudstorage.cloudstorage_audit_logs_enabled.cloudstorage_audit_logs_enabled import (
|
||||
cloudstorage_audit_logs_enabled,
|
||||
)
|
||||
|
||||
# Project has audit logs enabled but for a different service (not Cloud Storage)
|
||||
project = Project(
|
||||
id=GCP_PROJECT_ID,
|
||||
audit_logging=True,
|
||||
audit_configs=[
|
||||
AuditConfig(
|
||||
service="compute.googleapis.com",
|
||||
log_types=["DATA_READ", "DATA_WRITE"],
|
||||
)
|
||||
],
|
||||
)
|
||||
|
||||
cloudresourcemanager_client.cloud_resource_manager_projects = [project]
|
||||
cloudresourcemanager_client.region = GCP_US_CENTER1_LOCATION
|
||||
cloudresourcemanager_client.projects = {
|
||||
GCP_PROJECT_ID: GCPProject(
|
||||
id=GCP_PROJECT_ID,
|
||||
number="123456789012",
|
||||
name="test-project",
|
||||
labels={},
|
||||
lifecycle_state="ACTIVE",
|
||||
)
|
||||
}
|
||||
|
||||
check = cloudstorage_audit_logs_enabled()
|
||||
result = check.execute()
|
||||
|
||||
assert len(result) == 1
|
||||
assert result[0].status == "FAIL"
|
||||
assert result[0].status_extended == (
|
||||
f"Project {GCP_PROJECT_ID} has Audit Logs enabled for other services but not for Cloud Storage."
|
||||
)
|
||||
assert result[0].project_id == GCP_PROJECT_ID
|
||||
assert result[0].location == GCP_US_CENTER1_LOCATION
|
||||
assert result[0].resource_name == "test-project"
|
||||
|
||||
def test_project_without_audit_logs(self):
|
||||
cloudresourcemanager_client = mock.MagicMock()
|
||||
|
||||
with (
|
||||
mock.patch(
|
||||
"prowler.providers.common.provider.Provider.get_global_provider",
|
||||
return_value=set_mocked_gcp_provider(),
|
||||
),
|
||||
mock.patch(
|
||||
"prowler.providers.gcp.services.cloudstorage."
|
||||
"cloudstorage_audit_logs_enabled."
|
||||
"cloudstorage_audit_logs_enabled."
|
||||
"cloudresourcemanager_client",
|
||||
new=cloudresourcemanager_client,
|
||||
),
|
||||
):
|
||||
from prowler.providers.gcp.services.cloudresourcemanager.cloudresourcemanager_service import (
|
||||
Project,
|
||||
)
|
||||
from prowler.providers.gcp.services.cloudstorage.cloudstorage_audit_logs_enabled.cloudstorage_audit_logs_enabled import (
|
||||
cloudstorage_audit_logs_enabled,
|
||||
)
|
||||
|
||||
project = Project(
|
||||
id=GCP_PROJECT_ID,
|
||||
audit_logging=False,
|
||||
audit_configs=[],
|
||||
)
|
||||
|
||||
cloudresourcemanager_client.cloud_resource_manager_projects = [project]
|
||||
cloudresourcemanager_client.region = GCP_US_CENTER1_LOCATION
|
||||
cloudresourcemanager_client.projects = {
|
||||
GCP_PROJECT_ID: GCPProject(
|
||||
id=GCP_PROJECT_ID,
|
||||
number="123456789012",
|
||||
name="test-project",
|
||||
labels={},
|
||||
lifecycle_state="ACTIVE",
|
||||
)
|
||||
}
|
||||
|
||||
check = cloudstorage_audit_logs_enabled()
|
||||
result = check.execute()
|
||||
|
||||
assert len(result) == 1
|
||||
assert result[0].status == "FAIL"
|
||||
assert result[0].status_extended == (
|
||||
f"Project {GCP_PROJECT_ID} does not have Audit Logs enabled."
|
||||
)
|
||||
assert result[0].project_id == GCP_PROJECT_ID
|
||||
assert result[0].location == GCP_US_CENTER1_LOCATION
|
||||
assert result[0].resource_name == "test-project"
|
||||
|
||||
def test_project_with_missing_log_types(self):
|
||||
cloudresourcemanager_client = mock.MagicMock()
|
||||
|
||||
with (
|
||||
mock.patch(
|
||||
"prowler.providers.common.provider.Provider.get_global_provider",
|
||||
return_value=set_mocked_gcp_provider(),
|
||||
),
|
||||
mock.patch(
|
||||
"prowler.providers.gcp.services.cloudstorage."
|
||||
"cloudstorage_audit_logs_enabled."
|
||||
"cloudstorage_audit_logs_enabled."
|
||||
"cloudresourcemanager_client",
|
||||
new=cloudresourcemanager_client,
|
||||
),
|
||||
):
|
||||
from prowler.providers.gcp.services.cloudresourcemanager.cloudresourcemanager_service import (
|
||||
AuditConfig,
|
||||
Project,
|
||||
)
|
||||
from prowler.providers.gcp.services.cloudstorage.cloudstorage_audit_logs_enabled.cloudstorage_audit_logs_enabled import (
|
||||
cloudstorage_audit_logs_enabled,
|
||||
)
|
||||
|
||||
project = Project(
|
||||
id=GCP_PROJECT_ID,
|
||||
audit_logging=True,
|
||||
audit_configs=[
|
||||
AuditConfig(
|
||||
service="storage.googleapis.com",
|
||||
log_types=["DATA_WRITE"],
|
||||
)
|
||||
],
|
||||
)
|
||||
|
||||
cloudresourcemanager_client.cloud_resource_manager_projects = [project]
|
||||
cloudresourcemanager_client.region = GCP_US_CENTER1_LOCATION
|
||||
cloudresourcemanager_client.projects = {
|
||||
GCP_PROJECT_ID: GCPProject(
|
||||
id=GCP_PROJECT_ID,
|
||||
number="123456789012",
|
||||
name="test-project",
|
||||
labels={},
|
||||
lifecycle_state="ACTIVE",
|
||||
)
|
||||
}
|
||||
|
||||
check = cloudstorage_audit_logs_enabled()
|
||||
result = check.execute()
|
||||
|
||||
assert len(result) == 1
|
||||
assert result[0].status == "FAIL"
|
||||
assert result[0].status_extended == (
|
||||
f"Project {GCP_PROJECT_ID} has Audit Logs enabled for Cloud Storage but is missing some required log types"
|
||||
f"(missing: DATA_READ)."
|
||||
)
|
||||
assert result[0].project_id == GCP_PROJECT_ID
|
||||
assert result[0].location == GCP_US_CENTER1_LOCATION
|
||||
assert result[0].resource_name == "test-project"
|
||||
|
||||
def test_project_with_combined_audit_configs(self):
|
||||
cloudresourcemanager_client = mock.MagicMock()
|
||||
|
||||
with (
|
||||
mock.patch(
|
||||
"prowler.providers.common.provider.Provider.get_global_provider",
|
||||
return_value=set_mocked_gcp_provider(),
|
||||
),
|
||||
mock.patch(
|
||||
"prowler.providers.gcp.services.cloudstorage."
|
||||
"cloudstorage_audit_logs_enabled."
|
||||
"cloudstorage_audit_logs_enabled."
|
||||
"cloudresourcemanager_client",
|
||||
new=cloudresourcemanager_client,
|
||||
),
|
||||
):
|
||||
from prowler.providers.gcp.services.cloudresourcemanager.cloudresourcemanager_service import (
|
||||
AuditConfig,
|
||||
Project,
|
||||
)
|
||||
from prowler.providers.gcp.services.cloudstorage.cloudstorage_audit_logs_enabled.cloudstorage_audit_logs_enabled import (
|
||||
cloudstorage_audit_logs_enabled,
|
||||
)
|
||||
|
||||
# Project has both allServices (with DATA_READ)
|
||||
# and storage.googleapis.com (with DATA_WRITE)
|
||||
project = Project(
|
||||
id=GCP_PROJECT_ID,
|
||||
audit_logging=True,
|
||||
audit_configs=[
|
||||
AuditConfig(
|
||||
service="allServices",
|
||||
log_types=["DATA_READ"],
|
||||
),
|
||||
AuditConfig(
|
||||
service="storage.googleapis.com",
|
||||
log_types=["DATA_WRITE"],
|
||||
),
|
||||
],
|
||||
)
|
||||
|
||||
cloudresourcemanager_client.cloud_resource_manager_projects = [project]
|
||||
cloudresourcemanager_client.region = GCP_US_CENTER1_LOCATION
|
||||
cloudresourcemanager_client.projects = {
|
||||
GCP_PROJECT_ID: GCPProject(
|
||||
id=GCP_PROJECT_ID,
|
||||
number="123456789012",
|
||||
name="test-project",
|
||||
labels={},
|
||||
lifecycle_state="ACTIVE",
|
||||
)
|
||||
}
|
||||
|
||||
check = cloudstorage_audit_logs_enabled()
|
||||
result = check.execute()
|
||||
|
||||
assert len(result) == 1
|
||||
assert result[0].status == "PASS"
|
||||
assert result[0].status_extended == (
|
||||
f"Project {GCP_PROJECT_ID} has Data Access audit logs "
|
||||
f"(DATA_READ and DATA_WRITE) enabled for Cloud Storage."
|
||||
)
|
||||
assert result[0].project_id == GCP_PROJECT_ID
|
||||
assert result[0].location == GCP_US_CENTER1_LOCATION
|
||||
assert result[0].resource_name == "test-project"
|
||||
|
||||
def test_project_with_allservices_audit_config(self):
|
||||
cloudresourcemanager_client = mock.MagicMock()
|
||||
|
||||
with (
|
||||
mock.patch(
|
||||
"prowler.providers.common.provider.Provider.get_global_provider",
|
||||
return_value=set_mocked_gcp_provider(),
|
||||
),
|
||||
mock.patch(
|
||||
"prowler.providers.gcp.services.cloudstorage."
|
||||
"cloudstorage_audit_logs_enabled."
|
||||
"cloudstorage_audit_logs_enabled."
|
||||
"cloudresourcemanager_client",
|
||||
new=cloudresourcemanager_client,
|
||||
),
|
||||
):
|
||||
from prowler.providers.gcp.services.cloudresourcemanager.cloudresourcemanager_service import (
|
||||
AuditConfig,
|
||||
Project,
|
||||
)
|
||||
from prowler.providers.gcp.services.cloudstorage.cloudstorage_audit_logs_enabled.cloudstorage_audit_logs_enabled import (
|
||||
cloudstorage_audit_logs_enabled,
|
||||
)
|
||||
|
||||
# Project has allServices with both log types
|
||||
project = Project(
|
||||
id=GCP_PROJECT_ID,
|
||||
audit_logging=True,
|
||||
audit_configs=[
|
||||
AuditConfig(
|
||||
service="allServices",
|
||||
log_types=["DATA_READ", "DATA_WRITE"],
|
||||
)
|
||||
],
|
||||
)
|
||||
|
||||
cloudresourcemanager_client.cloud_resource_manager_projects = [project]
|
||||
cloudresourcemanager_client.region = GCP_US_CENTER1_LOCATION
|
||||
cloudresourcemanager_client.projects = {
|
||||
GCP_PROJECT_ID: GCPProject(
|
||||
id=GCP_PROJECT_ID,
|
||||
number="123456789012",
|
||||
name="test-project",
|
||||
labels={},
|
||||
lifecycle_state="ACTIVE",
|
||||
)
|
||||
}
|
||||
|
||||
check = cloudstorage_audit_logs_enabled()
|
||||
result = check.execute()
|
||||
|
||||
assert len(result) == 1
|
||||
assert result[0].status == "PASS"
|
||||
assert result[0].status_extended == (
|
||||
f"Project {GCP_PROJECT_ID} has Data Access audit logs "
|
||||
f"(DATA_READ and DATA_WRITE) enabled for Cloud Storage."
|
||||
)
|
||||
assert result[0].project_id == GCP_PROJECT_ID
|
||||
assert result[0].location == GCP_US_CENTER1_LOCATION
|
||||
assert result[0].resource_name == "test-project"
|
||||
@@ -148,31 +148,19 @@ class TestIacProvider:
|
||||
@mock.patch.dict(os.environ, {}, clear=True)
|
||||
def test_provider_run_remote_scan(self):
|
||||
scan_repository_url = "https://github.com/user/repo"
|
||||
provider = IacProvider(scan_repository_url=scan_repository_url)
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
with (
|
||||
mock.patch(
|
||||
"prowler.providers.iac.iac_provider.IacProvider._clone_repository",
|
||||
return_value=(temp_dir, "main"),
|
||||
return_value=temp_dir,
|
||||
) as mock_clone,
|
||||
mock.patch(
|
||||
"prowler.providers.iac.iac_provider.IacProvider.run_scan"
|
||||
) as mock_run_scan,
|
||||
):
|
||||
# Repository cloning now happens during __init__
|
||||
provider = IacProvider(scan_repository_url=scan_repository_url)
|
||||
|
||||
# Verify clone was called during initialization
|
||||
mock_clone.assert_called_once_with(
|
||||
scan_repository_url, None, None, None
|
||||
)
|
||||
|
||||
# Verify region was updated with branch name
|
||||
assert provider.region == "main"
|
||||
|
||||
# Run the scan
|
||||
provider.run()
|
||||
|
||||
# Verify scan was called with the cloned directory
|
||||
mock_clone.assert_called_with(scan_repository_url, None, None, None)
|
||||
mock_run_scan.assert_called_with(
|
||||
temp_dir, ["vuln", "misconfig", "secret"], []
|
||||
)
|
||||
@@ -195,22 +183,17 @@ class TestIacProvider:
|
||||
@mock.patch.dict(os.environ, {}, clear=True)
|
||||
def test_print_credentials_remote(self):
|
||||
repo_url = "https://github.com/user/repo"
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
with mock.patch(
|
||||
"prowler.providers.iac.iac_provider.IacProvider._clone_repository",
|
||||
return_value=(temp_dir, "main"),
|
||||
):
|
||||
provider = IacProvider(scan_repository_url=repo_url)
|
||||
with mock.patch("builtins.print") as mock_print:
|
||||
provider.print_credentials()
|
||||
assert any(
|
||||
f"Repository: \x1b[33m{repo_url}\x1b[0m" in call.args[0]
|
||||
for call in mock_print.call_args_list
|
||||
)
|
||||
assert any(
|
||||
"Scanning remote IaC repository:" in call.args[0]
|
||||
for call in mock_print.call_args_list
|
||||
)
|
||||
provider = IacProvider(scan_repository_url=repo_url)
|
||||
with mock.patch("builtins.print") as mock_print:
|
||||
provider.print_credentials()
|
||||
assert any(
|
||||
f"Repository: \x1b[33m{repo_url}\x1b[0m" in call.args[0]
|
||||
for call in mock_print.call_args_list
|
||||
)
|
||||
assert any(
|
||||
"Scanning remote IaC repository:" in call.args[0]
|
||||
for call in mock_print.call_args_list
|
||||
)
|
||||
|
||||
@patch("subprocess.run")
|
||||
def test_iac_provider_process_check_medium_severity(self, mock_subprocess):
|
||||
@@ -681,79 +664,25 @@ class TestIacProvider:
|
||||
def test_clone_repository_no_auth(self, _mock_mkdtemp, mock_clone):
|
||||
provider = IacProvider()
|
||||
url = "https://github.com/user/repo.git"
|
||||
with mock.patch.object(provider, "_detect_branch_name", return_value="main"):
|
||||
temp_dir, branch_name = provider._clone_repository(url)
|
||||
provider._clone_repository(url)
|
||||
mock_clone.assert_called_with(url, "/tmp/fake-dir", depth=1)
|
||||
assert temp_dir == "/tmp/fake-dir"
|
||||
assert branch_name == "main"
|
||||
|
||||
@mock.patch("prowler.providers.iac.iac_provider.porcelain.clone")
|
||||
@mock.patch("tempfile.mkdtemp", return_value="/tmp/fake-dir")
|
||||
def test_clone_repository_with_pat(self, _mock_mkdtemp, mock_clone):
|
||||
provider = IacProvider()
|
||||
url = "https://github.com/user/repo.git"
|
||||
with mock.patch.object(provider, "_detect_branch_name", return_value="develop"):
|
||||
temp_dir, branch_name = provider._clone_repository(
|
||||
url, github_username="user", personal_access_token="token123"
|
||||
)
|
||||
provider._clone_repository(
|
||||
url, github_username="user", personal_access_token="token123"
|
||||
)
|
||||
expected_url = "https://user:token123@github.com/user/repo.git"
|
||||
mock_clone.assert_called_with(expected_url, "/tmp/fake-dir", depth=1)
|
||||
assert temp_dir == "/tmp/fake-dir"
|
||||
assert branch_name == "develop"
|
||||
|
||||
@mock.patch("prowler.providers.iac.iac_provider.porcelain.clone")
|
||||
@mock.patch("tempfile.mkdtemp", return_value="/tmp/fake-dir")
|
||||
def test_clone_repository_with_oauth(self, _mock_mkdtemp, mock_clone):
|
||||
provider = IacProvider()
|
||||
url = "https://github.com/user/repo.git"
|
||||
with mock.patch.object(provider, "_detect_branch_name", return_value="master"):
|
||||
temp_dir, branch_name = provider._clone_repository(
|
||||
url, oauth_app_token="oauth456"
|
||||
)
|
||||
provider._clone_repository(url, oauth_app_token="oauth456")
|
||||
expected_url = "https://oauth2:oauth456@github.com/user/repo.git"
|
||||
mock_clone.assert_called_with(expected_url, "/tmp/fake-dir", depth=1)
|
||||
assert temp_dir == "/tmp/fake-dir"
|
||||
assert branch_name == "master"
|
||||
|
||||
def test_detect_branch_name_main(self):
|
||||
"""Test detecting 'main' branch from .git/HEAD"""
|
||||
provider = IacProvider()
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
# Create a mock .git/HEAD file with main branch
|
||||
git_dir = os.path.join(temp_dir, ".git")
|
||||
os.makedirs(git_dir)
|
||||
head_file = os.path.join(git_dir, "HEAD")
|
||||
with open(head_file, "w") as f:
|
||||
f.write("ref: refs/heads/main\n")
|
||||
|
||||
branch_name = provider._detect_branch_name(temp_dir)
|
||||
assert branch_name == "main"
|
||||
|
||||
def test_detect_branch_name_custom_branch(self):
|
||||
"""Test detecting custom branch like 'develop' from .git/HEAD"""
|
||||
provider = IacProvider()
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
# Create a mock .git/HEAD file with develop branch
|
||||
git_dir = os.path.join(temp_dir, ".git")
|
||||
os.makedirs(git_dir)
|
||||
head_file = os.path.join(git_dir, "HEAD")
|
||||
with open(head_file, "w") as f:
|
||||
f.write("ref: refs/heads/develop\n")
|
||||
|
||||
branch_name = provider._detect_branch_name(temp_dir)
|
||||
assert branch_name == "develop"
|
||||
|
||||
def test_detect_branch_name_fallback(self):
|
||||
"""Test fallback to 'main' when .git/HEAD doesn't exist"""
|
||||
provider = IacProvider()
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
# Don't create .git/HEAD file
|
||||
branch_name = provider._detect_branch_name(temp_dir)
|
||||
assert branch_name == "main"
|
||||
|
||||
def test_detect_branch_name_error_handling(self):
|
||||
"""Test error handling returns 'main' as fallback"""
|
||||
provider = IacProvider()
|
||||
# Pass a non-existent directory
|
||||
branch_name = provider._detect_branch_name("/non/existent/path")
|
||||
assert branch_name == "main"
|
||||
|
||||
@@ -29,11 +29,9 @@ class Test_exchange_mailbox_policy_additional_storage_restricted:
|
||||
MailboxPolicy,
|
||||
)
|
||||
|
||||
exchange_client.mailbox_policies = [
|
||||
MailboxPolicy(
|
||||
id="OwaMailboxPolicy-Default", additional_storage_enabled=False
|
||||
)
|
||||
]
|
||||
exchange_client.mailbox_policy = MailboxPolicy(
|
||||
id="OwaMailboxPolicy-Default", additional_storage_enabled=False
|
||||
)
|
||||
|
||||
check = exchange_mailbox_policy_additional_storage_restricted()
|
||||
result = check.execute()
|
||||
@@ -42,13 +40,10 @@ class Test_exchange_mailbox_policy_additional_storage_restricted:
|
||||
assert result[0].status == "PASS"
|
||||
assert (
|
||||
result[0].status_extended
|
||||
== "Exchange mailbox policy 'OwaMailboxPolicy-Default' restricts additional storage providers."
|
||||
)
|
||||
assert result[0].resource == exchange_client.mailbox_policies[0].dict()
|
||||
assert (
|
||||
result[0].resource_name
|
||||
== "Exchange Mailbox Policy - OwaMailboxPolicy-Default"
|
||||
== "Exchange mailbox policy restricts additional storage providers."
|
||||
)
|
||||
assert result[0].resource == exchange_client.mailbox_policy.dict()
|
||||
assert result[0].resource_name == "Exchange Mailbox Policy"
|
||||
assert result[0].resource_id == "OwaMailboxPolicy-Default"
|
||||
assert result[0].location == "global"
|
||||
|
||||
@@ -77,11 +72,9 @@ class Test_exchange_mailbox_policy_additional_storage_restricted:
|
||||
MailboxPolicy,
|
||||
)
|
||||
|
||||
exchange_client.mailbox_policies = [
|
||||
MailboxPolicy(
|
||||
id="OwaMailboxPolicy-Default", additional_storage_enabled=True
|
||||
)
|
||||
]
|
||||
exchange_client.mailbox_policy = MailboxPolicy(
|
||||
id="OwaMailboxPolicy-Default", additional_storage_enabled=True
|
||||
)
|
||||
|
||||
check = exchange_mailbox_policy_additional_storage_restricted()
|
||||
result = check.execute()
|
||||
@@ -90,13 +83,10 @@ class Test_exchange_mailbox_policy_additional_storage_restricted:
|
||||
assert result[0].status == "FAIL"
|
||||
assert (
|
||||
result[0].status_extended
|
||||
== "Exchange mailbox policy 'OwaMailboxPolicy-Default' allows additional storage providers."
|
||||
)
|
||||
assert result[0].resource == exchange_client.mailbox_policies[0].dict()
|
||||
assert (
|
||||
result[0].resource_name
|
||||
== "Exchange Mailbox Policy - OwaMailboxPolicy-Default"
|
||||
== "Exchange mailbox policy allows additional storage providers."
|
||||
)
|
||||
assert result[0].resource == exchange_client.mailbox_policy.dict()
|
||||
assert result[0].resource_name == "Exchange Mailbox Policy"
|
||||
assert result[0].resource_id == "OwaMailboxPolicy-Default"
|
||||
assert result[0].location == "global"
|
||||
|
||||
@@ -104,7 +94,7 @@ class Test_exchange_mailbox_policy_additional_storage_restricted:
|
||||
exchange_client = mock.MagicMock()
|
||||
exchange_client.audited_tenant = "audited_tenant"
|
||||
exchange_client.audited_domain = DOMAIN
|
||||
exchange_client.mailbox_policies = []
|
||||
exchange_client.mailbox_policy = None
|
||||
|
||||
with (
|
||||
mock.patch(
|
||||
@@ -126,57 +116,3 @@ class Test_exchange_mailbox_policy_additional_storage_restricted:
|
||||
check = exchange_mailbox_policy_additional_storage_restricted()
|
||||
result = check.execute()
|
||||
assert len(result) == 0
|
||||
|
||||
def test_multiple_mailbox_policies_mixed_results(self):
|
||||
exchange_client = mock.MagicMock()
|
||||
exchange_client.audited_tenant = "audited_tenant"
|
||||
exchange_client.audited_domain = DOMAIN
|
||||
|
||||
with (
|
||||
mock.patch(
|
||||
"prowler.providers.common.provider.Provider.get_global_provider",
|
||||
return_value=set_mocked_m365_provider(),
|
||||
),
|
||||
mock.patch(
|
||||
"prowler.providers.m365.lib.powershell.m365_powershell.M365PowerShell.connect_exchange_online"
|
||||
),
|
||||
mock.patch(
|
||||
"prowler.providers.m365.services.exchange.exchange_mailbox_policy_additional_storage_restricted.exchange_mailbox_policy_additional_storage_restricted.exchange_client",
|
||||
new=exchange_client,
|
||||
),
|
||||
):
|
||||
from prowler.providers.m365.services.exchange.exchange_mailbox_policy_additional_storage_restricted.exchange_mailbox_policy_additional_storage_restricted import (
|
||||
exchange_mailbox_policy_additional_storage_restricted,
|
||||
)
|
||||
from prowler.providers.m365.services.exchange.exchange_service import (
|
||||
MailboxPolicy,
|
||||
)
|
||||
|
||||
exchange_client.mailbox_policies = [
|
||||
MailboxPolicy(
|
||||
id="OwaMailboxPolicy-Default", additional_storage_enabled=False
|
||||
),
|
||||
MailboxPolicy(id="OWA-Policy-2", additional_storage_enabled=True),
|
||||
MailboxPolicy(id="OWA-Policy-3", additional_storage_enabled=False),
|
||||
]
|
||||
|
||||
check = exchange_mailbox_policy_additional_storage_restricted()
|
||||
result = check.execute()
|
||||
|
||||
# Should have 3 results, one for each policy
|
||||
assert len(result) == 3
|
||||
|
||||
# First policy (Default) should PASS
|
||||
assert result[0].status == "PASS"
|
||||
assert result[0].resource_id == "OwaMailboxPolicy-Default"
|
||||
assert "restricts additional storage providers" in result[0].status_extended
|
||||
|
||||
# Second policy should FAIL
|
||||
assert result[1].status == "FAIL"
|
||||
assert result[1].resource_id == "OWA-Policy-2"
|
||||
assert "allows additional storage providers" in result[1].status_extended
|
||||
|
||||
# Third policy should PASS
|
||||
assert result[2].status == "PASS"
|
||||
assert result[2].resource_id == "OWA-Policy-3"
|
||||
assert "restricts additional storage providers" in result[2].status_extended
|
||||
|
||||
@@ -7,6 +7,7 @@ from prowler.providers.m365.services.exchange.exchange_service import (
|
||||
ExternalMailConfig,
|
||||
MailboxAuditConfig,
|
||||
MailboxAuditProperties,
|
||||
MailboxPolicy,
|
||||
Organization,
|
||||
RoleAssignmentPolicy,
|
||||
TransportConfig,
|
||||
@@ -71,6 +72,13 @@ def mock_exchange_get_transport_config(_):
|
||||
)
|
||||
|
||||
|
||||
def mock_exchange_get_mailbox_policy(_):
|
||||
return MailboxPolicy(
|
||||
id="test",
|
||||
additional_storage_enabled=True,
|
||||
)
|
||||
|
||||
|
||||
def mock_exchange_get_role_assignment_policies(_):
|
||||
return [
|
||||
RoleAssignmentPolicy(
|
||||
@@ -264,19 +272,13 @@ class Test_Exchange_Service:
|
||||
exchange_client.powershell.close()
|
||||
|
||||
@patch(
|
||||
"prowler.providers.m365.lib.powershell.m365_powershell.M365PowerShell.get_mailbox_policy",
|
||||
return_value=[
|
||||
{
|
||||
"Id": "test",
|
||||
"AdditionalStorageProvidersAvailable": True,
|
||||
}
|
||||
],
|
||||
"prowler.providers.m365.services.exchange.exchange_service.Exchange._get_mailbox_policy",
|
||||
new=mock_exchange_get_mailbox_policy,
|
||||
)
|
||||
def test_get_mailbox_policy(self, _mock_get_mailbox_policy):
|
||||
def test_get_mailbox_policy(self):
|
||||
with (
|
||||
mock.patch(
|
||||
"prowler.providers.m365.lib.powershell.m365_powershell.M365PowerShell.connect_exchange_online",
|
||||
return_value=True,
|
||||
"prowler.providers.m365.lib.powershell.m365_powershell.M365PowerShell.connect_exchange_online"
|
||||
),
|
||||
):
|
||||
exchange_client = Exchange(
|
||||
@@ -284,35 +286,9 @@ class Test_Exchange_Service:
|
||||
identity=M365IdentityInfo(tenant_domain=DOMAIN)
|
||||
)
|
||||
)
|
||||
mailbox_policies = exchange_client.mailbox_policies
|
||||
assert len(mailbox_policies) == 1
|
||||
assert mailbox_policies[0].id == "test"
|
||||
assert mailbox_policies[0].additional_storage_enabled is True
|
||||
exchange_client.powershell.close()
|
||||
|
||||
@patch(
|
||||
"prowler.providers.m365.lib.powershell.m365_powershell.M365PowerShell.get_mailbox_policy",
|
||||
return_value={
|
||||
"Id": "test_single",
|
||||
"AdditionalStorageProvidersAvailable": False,
|
||||
},
|
||||
)
|
||||
def test_get_mailbox_policy_single_dict(self, _mock_get_mailbox_policy):
|
||||
with (
|
||||
mock.patch(
|
||||
"prowler.providers.m365.lib.powershell.m365_powershell.M365PowerShell.connect_exchange_online",
|
||||
return_value=True,
|
||||
),
|
||||
):
|
||||
exchange_client = Exchange(
|
||||
set_mocked_m365_provider(
|
||||
identity=M365IdentityInfo(tenant_domain=DOMAIN)
|
||||
)
|
||||
)
|
||||
mailbox_policies = exchange_client.mailbox_policies
|
||||
assert len(mailbox_policies) == 1
|
||||
assert mailbox_policies[0].id == "test_single"
|
||||
assert mailbox_policies[0].additional_storage_enabled is False
|
||||
mailbox_policy = exchange_client.mailbox_policy
|
||||
assert mailbox_policy.id == "test"
|
||||
assert mailbox_policy.additional_storage_enabled is True
|
||||
exchange_client.powershell.close()
|
||||
|
||||
@patch(
|
||||
|
||||
@@ -55,53 +55,6 @@ class Test_sharepoint_external_sharing_managed:
|
||||
assert result[0].resource_name == "SharePoint Settings"
|
||||
assert result[0].resource == sharepoint_client.settings.dict()
|
||||
|
||||
def test_external_sharing_disabled(self):
|
||||
"""
|
||||
Test when external sharing is disabled at organization level:
|
||||
The check should PASS since domain restrictions are not applicable.
|
||||
"""
|
||||
sharepoint_client = mock.MagicMock
|
||||
|
||||
with (
|
||||
mock.patch(
|
||||
"prowler.providers.common.provider.Provider.get_global_provider",
|
||||
return_value=set_mocked_m365_provider(),
|
||||
),
|
||||
mock.patch("prowler.providers.m365.lib.service.service.M365PowerShell"),
|
||||
mock.patch(
|
||||
"prowler.providers.m365.services.sharepoint.sharepoint_external_sharing_managed.sharepoint_external_sharing_managed.sharepoint_client",
|
||||
new=sharepoint_client,
|
||||
),
|
||||
):
|
||||
from prowler.providers.m365.services.sharepoint.sharepoint_external_sharing_managed.sharepoint_external_sharing_managed import (
|
||||
sharepoint_external_sharing_managed,
|
||||
)
|
||||
|
||||
sharepoint_client.settings = SharePointSettings(
|
||||
sharingCapability="Disabled",
|
||||
sharingAllowedDomainList=[],
|
||||
sharingBlockedDomainList=[],
|
||||
legacyAuth=True,
|
||||
resharingEnabled=False,
|
||||
sharingDomainRestrictionMode="none",
|
||||
allowedDomainGuidsForSyncApp=[uuid.uuid4()],
|
||||
)
|
||||
sharepoint_client.tenant_domain = DOMAIN
|
||||
|
||||
check = sharepoint_external_sharing_managed()
|
||||
result = check.execute()
|
||||
|
||||
assert len(result) == 1
|
||||
assert result[0].status == "PASS"
|
||||
assert (
|
||||
result[0].status_extended
|
||||
== "External sharing is disabled at organization level."
|
||||
)
|
||||
assert result[0].resource_id == "sharepointSettings"
|
||||
assert result[0].location == "global"
|
||||
assert result[0].resource_name == "SharePoint Settings"
|
||||
assert result[0].resource == sharepoint_client.settings.dict()
|
||||
|
||||
def test_allow_list_empty(self):
|
||||
"""
|
||||
Test when sharingDomainRestrictionMode is "allowList" but AllowedDomainList is empty:
|
||||
|
||||
@@ -13,5 +13,3 @@ README.md
|
||||
!.next/static
|
||||
!.next/standalone
|
||||
.git
|
||||
.husky
|
||||
scripts/setup-git-hooks.js
|
||||
|
||||
@@ -48,21 +48,23 @@ if [ "$CODE_REVIEW_ENABLED" = "true" ]; then
|
||||
echo -e "${YELLOW}🔍 Running Claude Code standards validation...${NC}"
|
||||
echo ""
|
||||
echo -e "${BLUE}📋 Files to validate:${NC}"
|
||||
echo "$STAGED_FILES" | while IFS= read -r file; do echo " - $file"; done
|
||||
echo "$STAGED_FILES" | sed 's/^/ - /'
|
||||
echo ""
|
||||
|
||||
echo -e "${BLUE}📤 Sending to Claude Code for validation...${NC}"
|
||||
echo ""
|
||||
|
||||
# Build prompt with full file contents
|
||||
# Build prompt with git diff of changes AND full context
|
||||
VALIDATION_PROMPT=$(
|
||||
cat <<'PROMPT_EOF'
|
||||
You are a code reviewer for the Prowler UI project. Analyze the full file contents of changed files below and validate they comply with AGENTS.md standards.
|
||||
You are a code reviewer for the Prowler UI project. Analyze the code changes (git diff with full context) below and validate they comply with AGENTS.md standards.
|
||||
|
||||
**CRITICAL: You MUST check BOTH the changed lines AND the surrounding context for violations.**
|
||||
|
||||
**RULES TO CHECK:**
|
||||
1. React Imports: NO `import * as React` or `import React, {` → Use `import { useState }`
|
||||
2. TypeScript: NO union types like `type X = "a" | "b"` → Use const-based: `const X = {...} as const`
|
||||
3. Tailwind: NO `var()` or hex colors in className → Use Tailwind utilities and semantic color classes.
|
||||
3. Tailwind: NO `var()` or hex colors in className → Use Tailwind utilities and semantic color classes (e.g., `bg-bg-neutral-tertiary`, `border-border-neutral-primary`)
|
||||
4. cn(): Use for merging multiple classes or for conditionals (handles Tailwind conflicts with twMerge) → `cn(BUTTON_STYLES.base, BUTTON_STYLES.active, isLoading && "opacity-50")`
|
||||
5. React 19: NO `useMemo`/`useCallback` without reason
|
||||
6. Zod v4: Use `.min(1)` not `.nonempty()`, `z.email()` not `z.string().email()`. All inputs must be validated with Zod.
|
||||
@@ -74,28 +76,24 @@ You are a code reviewer for the Prowler UI project. Analyze the full file conten
|
||||
12. Use the components inside components/shadcn if possible
|
||||
13. Check Accessibility best practices (like alt tags in images, semantic HTML, Aria labels, etc.)
|
||||
|
||||
=== FILES TO REVIEW ===
|
||||
=== GIT DIFF WITH CONTEXT ===
|
||||
PROMPT_EOF
|
||||
)
|
||||
|
||||
# Add full file contents for each staged file
|
||||
for file in $STAGED_FILES; do
|
||||
VALIDATION_PROMPT="$VALIDATION_PROMPT
|
||||
|
||||
=== FILE: $file ===
|
||||
$(cat "$file" 2>/dev/null || echo "Error reading file")"
|
||||
done
|
||||
# Add git diff to prompt with more context (U5 = 5 lines before/after)
|
||||
VALIDATION_PROMPT="$VALIDATION_PROMPT
|
||||
$(git diff --cached -U5)"
|
||||
|
||||
VALIDATION_PROMPT="$VALIDATION_PROMPT
|
||||
|
||||
=== END FILES ===
|
||||
=== END DIFF ===
|
||||
|
||||
**IMPORTANT: Your response MUST start with exactly one of these lines:**
|
||||
STATUS: PASSED
|
||||
STATUS: FAILED
|
||||
|
||||
**If FAILED:** List each violation with File, Line Number, Rule Number, and Issue.
|
||||
**If PASSED:** Confirm all files comply with AGENTS.md standards.
|
||||
**If PASSED:** Confirm all visible code (including context) complies with AGENTS.md standards.
|
||||
|
||||
**Start your response now with STATUS:**"
|
||||
|
||||
@@ -151,19 +149,3 @@ else
|
||||
echo ""
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Run build
|
||||
echo -e "${BLUE}🔨 Running build...${NC}"
|
||||
echo ""
|
||||
|
||||
if npm run build; then
|
||||
echo ""
|
||||
echo -e "${GREEN}✅ Build passed${NC}"
|
||||
echo ""
|
||||
else
|
||||
echo ""
|
||||
echo -e "${RED}❌ Build failed${NC}"
|
||||
echo -e "${RED}Fix build errors before committing${NC}"
|
||||
echo ""
|
||||
exit 1
|
||||
fi
|
||||
|
||||