Compare commits

..
Author SHA1 Message Date
Alan Buscaglia f03d83872e refactor(ui): improve type safety in compliance module
- Add optional requirements property to Framework interface for flat structures
- Replace union type with const-based pattern for TopFailedDataType
- Remove all 'as any' casts from commons.tsx and mitre.tsx
- Extract helper functions (buildTopFailedResult, hasFlatStructure, incrementFailedCount) for DRY
- Type Maps properly with explicit generic parameters
- Add proper return types to findOrCreateCategory and findOrCreateControl
- Replace any[] with unknown[] in ENSAttributesMetadata
- Fix React import to use named imports (createElement, ReactNode)
2025-12-05 14:22:04 +01:00
Alan Buscaglia bb620022f5 fix(ui): use severity sum for failed findings count in Risk Plot
Ensure tooltip and horizontal bar chart show consistent numbers by
using the sum of severity counts instead of separate API field
2025-12-05 13:40:54 +01:00
Alan Buscaglia 27a81defec refactor(ui): simplify Risk Plot - use ThreatScore directly, remove mocks
KISS/DRY improvements:
- Remove unnecessary convertToRiskScore - use ThreatScore (0-100) directly
- Reuse RiskPlotPoint type from actions instead of duplicate ScatterPoint
- Remove CustomLegend wrapper - inline ChartLegend usage
- Remove mock data file (risk-plot-view.tsx)
- Rename 'Risk Score' to 'Threat Score' for consistency
- Update XAxis domain from 0-10 to 0-100
- Simplify handlers with functional updates
2025-12-05 13:28:05 +01:00
Alan Buscaglia a81293d2ea fix(ui): remove rounding from Risk Score conversion to preserve decimals 2025-12-05 13:22:34 +01:00
Alan Buscaglia 80427dd127 fix(ui): show raw Risk Score percentage without rounding 2025-12-05 13:20:20 +01:00
Alan Buscaglia 14e9506b87 fix(ui): simplify Risk Score tooltip format
- Remove dotted line separator
- Show percentage inline with Risk Score label
2025-12-05 13:19:53 +01:00
Alan Buscaglia 3e72d575d4 fix(ui): format Risk Score tooltip to match ThreatScore style
- Display Risk Score as percentage (0-100%) with dotted line separator
- Match visual style of ThreatScore tooltip component
2025-12-05 13:17:44 +01:00
Alan Buscaglia 79825d35fc docs(ui): add Risk Plot to CHANGELOG 2025-12-05 13:14:04 +01:00
Alan Buscaglia 6215c1ba46 fix(ui): correct Risk Score calculation and add severity percentages
- Fix Risk Score formula: higher score = better (same as ThreatScore)
- Add percentage calculation for severity breakdown in bar chart
- Improve type safety in createScatterDotShape with proper JSDoc
- Replace inline styles with Tailwind classes on HTML elements
- Add documentation explaining CSS variables usage for Recharts compatibility
2025-12-05 13:07:14 +01:00
75 changed files with 924 additions and 5313 deletions
+33 -61
View File
@@ -48,34 +48,8 @@ jobs:
id: set-short-sha
run: echo "short-sha=${GITHUB_SHA::7}" >> $GITHUB_OUTPUT
notify-release-started:
if: github.repository == 'prowler-cloud/prowler' && (github.event_name == 'release' || github.event_name == 'workflow_dispatch')
needs: setup
runs-on: ubuntu-latest
timeout-minutes: 5
outputs:
message-ts: ${{ steps.slack-notification.outputs.ts }}
steps:
- name: Checkout repository
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Notify container push started
id: slack-notification
uses: ./.github/actions/slack-notification
env:
SLACK_CHANNEL_ID: ${{ secrets.SLACK_PLATFORM_DEPLOYMENTS }}
COMPONENT: API
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"
container-build-push:
needs: [setup, notify-release-started]
if: always() && needs.setup.result == 'success' && (needs.notify-release-started.result == 'success' || needs.notify-release-started.result == 'skipped')
needs: setup
runs-on: ${{ matrix.runner }}
strategy:
matrix:
@@ -104,6 +78,21 @@ jobs:
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1
- name: Notify container push started
id: slack-notification-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: API
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 API container for ${{ matrix.arch }}
id: container-push
if: github.event_name == 'push' || github.event_name == 'release' || github.event_name == 'workflow_dispatch'
@@ -117,6 +106,23 @@ jobs:
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()
uses: ./.github/actions/slack-notification
env:
SLACK_CHANNEL_ID: ${{ secrets.SLACK_PLATFORM_DEPLOYMENTS }}
MESSAGE_TS: ${{ steps.slack-notification-started.outputs.ts }}
COMPONENT: API
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-completed.json"
step-outcome: ${{ steps.container-push.outcome }}
update-ts: ${{ steps.slack-notification-started.outputs.ts }}
# Create and push multi-architecture manifest
create-manifest:
needs: [setup, container-build-push]
@@ -163,40 +169,6 @@ jobs:
regctl tag delete "${{ env.PROWLERCLOUD_DOCKERHUB_REPOSITORY }}/${{ env.PROWLERCLOUD_DOCKERHUB_IMAGE }}:${{ needs.setup.outputs.short-sha }}-arm64" || true
echo "Cleanup completed"
notify-release-completed:
if: always() && needs.notify-release-started.result == 'success' && (github.event_name == 'release' || github.event_name == 'workflow_dispatch')
needs: [setup, notify-release-started, container-build-push, create-manifest]
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- name: Checkout repository
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Determine overall outcome
id: outcome
run: |
if [[ "${{ needs.container-build-push.result }}" == "success" && "${{ needs.create-manifest.result }}" == "success" ]]; then
echo "outcome=success" >> $GITHUB_OUTPUT
else
echo "outcome=failure" >> $GITHUB_OUTPUT
fi
- name: Notify container push completed
uses: ./.github/actions/slack-notification
env:
SLACK_CHANNEL_ID: ${{ secrets.SLACK_PLATFORM_DEPLOYMENTS }}
MESSAGE_TS: ${{ needs.notify-release-started.outputs.message-ts }}
COMPONENT: API
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-completed.json"
step-outcome: ${{ steps.outcome.outputs.outcome }}
update-ts: ${{ needs.notify-release-started.outputs.message-ts }}
trigger-deployment:
if: github.event_name == 'push'
needs: [setup, container-build-push]
+33 -61
View File
@@ -47,34 +47,8 @@ jobs:
id: set-short-sha
run: echo "short-sha=${GITHUB_SHA::7}" >> $GITHUB_OUTPUT
notify-release-started:
if: github.repository == 'prowler-cloud/prowler' && (github.event_name == 'release' || github.event_name == 'workflow_dispatch')
needs: setup
runs-on: ubuntu-latest
timeout-minutes: 5
outputs:
message-ts: ${{ steps.slack-notification.outputs.ts }}
steps:
- name: Checkout repository
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Notify container push started
id: slack-notification
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"
container-build-push:
needs: [setup, notify-release-started]
if: always() && needs.setup.result == 'success' && (needs.notify-release-started.result == 'success' || needs.notify-release-started.result == 'skipped')
needs: setup
runs-on: ${{ matrix.runner }}
strategy:
matrix:
@@ -102,6 +76,21 @@ jobs:
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1
- name: Notify container push started
id: slack-notification-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'
@@ -123,6 +112,23 @@ jobs:
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()
uses: ./.github/actions/slack-notification
env:
SLACK_CHANNEL_ID: ${{ secrets.SLACK_PLATFORM_DEPLOYMENTS }}
MESSAGE_TS: ${{ steps.slack-notification-started.outputs.ts }}
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-completed.json"
step-outcome: ${{ steps.container-push.outcome }}
update-ts: ${{ steps.slack-notification-started.outputs.ts }}
# Create and push multi-architecture manifest
create-manifest:
needs: [setup, container-build-push]
@@ -169,40 +175,6 @@ jobs:
regctl tag delete "${{ env.PROWLERCLOUD_DOCKERHUB_REPOSITORY }}/${{ env.PROWLERCLOUD_DOCKERHUB_IMAGE }}:${{ needs.setup.outputs.short-sha }}-arm64" || true
echo "Cleanup completed"
notify-release-completed:
if: always() && needs.notify-release-started.result == 'success' && (github.event_name == 'release' || github.event_name == 'workflow_dispatch')
needs: [setup, notify-release-started, container-build-push, create-manifest]
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- name: Checkout repository
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Determine overall outcome
id: outcome
run: |
if [[ "${{ needs.container-build-push.result }}" == "success" && "${{ needs.create-manifest.result }}" == "success" ]]; then
echo "outcome=success" >> $GITHUB_OUTPUT
else
echo "outcome=failure" >> $GITHUB_OUTPUT
fi
- name: Notify container push completed
uses: ./.github/actions/slack-notification
env:
SLACK_CHANNEL_ID: ${{ secrets.SLACK_PLATFORM_DEPLOYMENTS }}
MESSAGE_TS: ${{ needs.notify-release-started.outputs.message-ts }}
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-completed.json"
step-outcome: ${{ steps.outcome.outputs.outcome }}
update-ts: ${{ needs.notify-release-started.outputs.message-ts }}
trigger-deployment:
if: github.event_name == 'push'
needs: [setup, container-build-push]
+78 -104
View File
@@ -50,15 +50,30 @@ env:
AWS_REGION: us-east-1
jobs:
setup:
container-build-push:
if: github.repository == 'prowler-cloud/prowler'
runs-on: ubuntu-latest
timeout-minutes: 5
runs-on: ${{ matrix.runner }}
strategy:
matrix:
include:
- platform: linux/amd64
runner: ubuntu-latest
arch: amd64
- platform: linux/arm64
runner: ubuntu-24.04-arm
arch: arm64
timeout-minutes: 45
permissions:
contents: read
packages: write
outputs:
prowler_version: ${{ steps.get-prowler-version.outputs.prowler_version }}
prowler_version_major: ${{ steps.get-prowler-version.outputs.prowler_version_major }}
latest_tag: ${{ steps.get-prowler-version.outputs.latest_tag }}
stable_tag: ${{ steps.get-prowler-version.outputs.stable_tag }}
env:
POETRY_VIRTUALENVS_CREATE: 'false'
steps:
- name: Checkout repository
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
@@ -78,24 +93,32 @@ jobs:
run: |
PROWLER_VERSION="$(poetry version -s 2>/dev/null)"
echo "prowler_version=${PROWLER_VERSION}" >> "${GITHUB_OUTPUT}"
echo "PROWLER_VERSION=${PROWLER_VERSION}" >> "${GITHUB_ENV}"
# Extract major version
PROWLER_VERSION_MAJOR="${PROWLER_VERSION%%.*}"
echo "prowler_version_major=${PROWLER_VERSION_MAJOR}" >> "${GITHUB_OUTPUT}"
echo "PROWLER_VERSION_MAJOR=${PROWLER_VERSION_MAJOR}" >> "${GITHUB_ENV}"
# Set version-specific tags
case ${PROWLER_VERSION_MAJOR} in
3)
echo "LATEST_TAG=v3-latest" >> "${GITHUB_ENV}"
echo "STABLE_TAG=v3-stable" >> "${GITHUB_ENV}"
echo "latest_tag=v3-latest" >> "${GITHUB_OUTPUT}"
echo "stable_tag=v3-stable" >> "${GITHUB_OUTPUT}"
echo "✓ Prowler v3 detected - tags: v3-latest, v3-stable"
;;
4)
echo "LATEST_TAG=v4-latest" >> "${GITHUB_ENV}"
echo "STABLE_TAG=v4-stable" >> "${GITHUB_ENV}"
echo "latest_tag=v4-latest" >> "${GITHUB_OUTPUT}"
echo "stable_tag=v4-stable" >> "${GITHUB_OUTPUT}"
echo "✓ Prowler v4 detected - tags: v4-latest, v4-stable"
;;
5)
echo "LATEST_TAG=latest" >> "${GITHUB_ENV}"
echo "STABLE_TAG=stable" >> "${GITHUB_ENV}"
echo "latest_tag=latest" >> "${GITHUB_OUTPUT}"
echo "stable_tag=stable" >> "${GITHUB_OUTPUT}"
echo "✓ Prowler v5 detected - tags: latest, stable"
@@ -106,53 +129,6 @@ jobs:
;;
esac
notify-release-started:
if: github.repository == 'prowler-cloud/prowler' && (github.event_name == 'release' || github.event_name == 'workflow_dispatch')
needs: setup
runs-on: ubuntu-latest
timeout-minutes: 5
outputs:
message-ts: ${{ steps.slack-notification.outputs.ts }}
steps:
- name: Checkout repository
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Notify container push started
id: slack-notification
uses: ./.github/actions/slack-notification
env:
SLACK_CHANNEL_ID: ${{ secrets.SLACK_PLATFORM_DEPLOYMENTS }}
COMPONENT: SDK
RELEASE_TAG: ${{ needs.setup.outputs.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"
container-build-push:
needs: [setup, notify-release-started]
if: always() && needs.setup.result == 'success' && (needs.notify-release-started.result == 'success' || needs.notify-release-started.result == 'skipped')
runs-on: ${{ matrix.runner }}
strategy:
matrix:
include:
- platform: linux/amd64
runner: ubuntu-latest
arch: amd64
- platform: linux/arm64
runner: ubuntu-24.04-arm
arch: arm64
timeout-minutes: 45
permissions:
contents: read
packages: write
steps:
- name: Checkout repository
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Login to DockerHub
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
with:
@@ -171,6 +147,21 @@ jobs:
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1
- name: Notify container push started
id: slack-notification-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'
@@ -181,13 +172,30 @@ jobs:
push: true
platforms: ${{ matrix.platform }}
tags: |
${{ env.PROWLERCLOUD_DOCKERHUB_REPOSITORY }}/${{ env.PROWLERCLOUD_DOCKERHUB_IMAGE }}:${{ needs.setup.outputs.latest_tag }}-${{ matrix.arch }}
${{ env.PROWLERCLOUD_DOCKERHUB_REPOSITORY }}/${{ env.PROWLERCLOUD_DOCKERHUB_IMAGE }}:${{ env.LATEST_TAG }}-${{ matrix.arch }}
cache-from: type=gha,scope=${{ matrix.arch }}
cache-to: type=gha,mode=max,scope=${{ matrix.arch }}
- name: Notify container push completed
if: (github.event_name == 'release' || github.event_name == 'workflow_dispatch') && always()
uses: ./.github/actions/slack-notification
env:
SLACK_CHANNEL_ID: ${{ secrets.SLACK_PLATFORM_DEPLOYMENTS }}
MESSAGE_TS: ${{ steps.slack-notification-started.outputs.ts }}
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-completed.json"
step-outcome: ${{ steps.container-push.outcome }}
update-ts: ${{ steps.slack-notification-started.outputs.ts }}
# Create and push multi-architecture manifest
create-manifest:
needs: [setup, container-build-push]
needs: [container-build-push]
if: github.event_name == 'push' || github.event_name == 'release' || github.event_name == 'workflow_dispatch'
runs-on: ubuntu-latest
@@ -214,24 +222,24 @@ jobs:
if: github.event_name == 'push'
run: |
docker buildx imagetools create \
-t ${{ env.PROWLERCLOUD_DOCKERHUB_REPOSITORY }}/${{ env.PROWLERCLOUD_DOCKERHUB_IMAGE }}:${{ needs.setup.outputs.latest_tag }} \
-t ${{ secrets.DOCKER_HUB_REPOSITORY }}/${{ env.PROWLERCLOUD_DOCKERHUB_IMAGE }}:${{ needs.setup.outputs.latest_tag }} \
-t ${{ secrets.PUBLIC_ECR_REPOSITORY }}/${{ env.PROWLERCLOUD_DOCKERHUB_IMAGE }}:${{ needs.setup.outputs.latest_tag }} \
${{ env.PROWLERCLOUD_DOCKERHUB_REPOSITORY }}/${{ env.PROWLERCLOUD_DOCKERHUB_IMAGE }}:${{ needs.setup.outputs.latest_tag }}-amd64 \
${{ env.PROWLERCLOUD_DOCKERHUB_REPOSITORY }}/${{ env.PROWLERCLOUD_DOCKERHUB_IMAGE }}:${{ needs.setup.outputs.latest_tag }}-arm64
-t ${{ env.PROWLERCLOUD_DOCKERHUB_REPOSITORY }}/${{ env.PROWLERCLOUD_DOCKERHUB_IMAGE }}:${{ needs.container-build-push.outputs.latest_tag }} \
-t ${{ secrets.DOCKER_HUB_REPOSITORY }}/${{ env.PROWLERCLOUD_DOCKERHUB_IMAGE }}:${{ needs.container-build-push.outputs.latest_tag }} \
-t ${{ secrets.PUBLIC_ECR_REPOSITORY }}/${{ env.PROWLERCLOUD_DOCKERHUB_IMAGE }}:${{ needs.container-build-push.outputs.latest_tag }} \
${{ env.PROWLERCLOUD_DOCKERHUB_REPOSITORY }}/${{ env.PROWLERCLOUD_DOCKERHUB_IMAGE }}:${{ needs.container-build-push.outputs.latest_tag }}-amd64 \
${{ env.PROWLERCLOUD_DOCKERHUB_REPOSITORY }}/${{ env.PROWLERCLOUD_DOCKERHUB_IMAGE }}:${{ needs.container-build-push.outputs.latest_tag }}-arm64
- name: Create and push manifests for release event
if: github.event_name == 'release' || github.event_name == 'workflow_dispatch'
run: |
docker buildx imagetools create \
-t ${{ secrets.DOCKER_HUB_REPOSITORY }}/${{ env.IMAGE_NAME }}:${{ needs.setup.outputs.prowler_version }} \
-t ${{ secrets.DOCKER_HUB_REPOSITORY }}/${{ env.IMAGE_NAME }}:${{ needs.setup.outputs.stable_tag }} \
-t ${{ secrets.PUBLIC_ECR_REPOSITORY }}/${{ env.IMAGE_NAME }}:${{ needs.setup.outputs.prowler_version }} \
-t ${{ secrets.PUBLIC_ECR_REPOSITORY }}/${{ env.IMAGE_NAME }}:${{ needs.setup.outputs.stable_tag }} \
-t ${{ env.PROWLERCLOUD_DOCKERHUB_REPOSITORY }}/${{ env.PROWLERCLOUD_DOCKERHUB_IMAGE }}:${{ needs.setup.outputs.prowler_version }} \
-t ${{ env.PROWLERCLOUD_DOCKERHUB_REPOSITORY }}/${{ env.PROWLERCLOUD_DOCKERHUB_IMAGE }}:${{ needs.setup.outputs.stable_tag }} \
${{ env.PROWLERCLOUD_DOCKERHUB_REPOSITORY }}/${{ env.PROWLERCLOUD_DOCKERHUB_IMAGE }}:${{ needs.setup.outputs.latest_tag }}-amd64 \
${{ env.PROWLERCLOUD_DOCKERHUB_REPOSITORY }}/${{ env.PROWLERCLOUD_DOCKERHUB_IMAGE }}:${{ needs.setup.outputs.latest_tag }}-arm64
-t ${{ secrets.DOCKER_HUB_REPOSITORY }}/${{ env.IMAGE_NAME }}:${{ needs.container-build-push.outputs.prowler_version }} \
-t ${{ secrets.DOCKER_HUB_REPOSITORY }}/${{ env.IMAGE_NAME }}:${{ needs.container-build-push.outputs.stable_tag }} \
-t ${{ secrets.PUBLIC_ECR_REPOSITORY }}/${{ env.IMAGE_NAME }}:${{ needs.container-build-push.outputs.prowler_version }} \
-t ${{ secrets.PUBLIC_ECR_REPOSITORY }}/${{ env.IMAGE_NAME }}:${{ needs.container-build-push.outputs.stable_tag }} \
-t ${{ env.PROWLERCLOUD_DOCKERHUB_REPOSITORY }}/${{ env.PROWLERCLOUD_DOCKERHUB_IMAGE }}:${{ needs.container-build-push.outputs.prowler_version }} \
-t ${{ env.PROWLERCLOUD_DOCKERHUB_REPOSITORY }}/${{ env.PROWLERCLOUD_DOCKERHUB_IMAGE }}:${{ needs.container-build-push.outputs.stable_tag }} \
${{ env.PROWLERCLOUD_DOCKERHUB_REPOSITORY }}/${{ env.PROWLERCLOUD_DOCKERHUB_IMAGE }}:${{ needs.container-build-push.outputs.latest_tag }}-amd64 \
${{ env.PROWLERCLOUD_DOCKERHUB_REPOSITORY }}/${{ env.PROWLERCLOUD_DOCKERHUB_IMAGE }}:${{ needs.container-build-push.outputs.latest_tag }}-arm64
- name: Install regctl
if: always()
@@ -241,47 +249,13 @@ jobs:
if: always()
run: |
echo "Cleaning up intermediate tags..."
regctl tag delete "${{ env.PROWLERCLOUD_DOCKERHUB_REPOSITORY }}/${{ env.PROWLERCLOUD_DOCKERHUB_IMAGE }}:${{ needs.setup.outputs.latest_tag }}-amd64" || true
regctl tag delete "${{ env.PROWLERCLOUD_DOCKERHUB_REPOSITORY }}/${{ env.PROWLERCLOUD_DOCKERHUB_IMAGE }}:${{ needs.setup.outputs.latest_tag }}-arm64" || true
regctl tag delete "${{ env.PROWLERCLOUD_DOCKERHUB_REPOSITORY }}/${{ env.PROWLERCLOUD_DOCKERHUB_IMAGE }}:${{ needs.container-build-push.outputs.latest_tag }}-amd64" || true
regctl tag delete "${{ env.PROWLERCLOUD_DOCKERHUB_REPOSITORY }}/${{ env.PROWLERCLOUD_DOCKERHUB_IMAGE }}:${{ needs.container-build-push.outputs.latest_tag }}-arm64" || true
echo "Cleanup completed"
notify-release-completed:
if: always() && needs.notify-release-started.result == 'success' && (github.event_name == 'release' || github.event_name == 'workflow_dispatch')
needs: [setup, notify-release-started, container-build-push, create-manifest]
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- name: Checkout repository
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Determine overall outcome
id: outcome
run: |
if [[ "${{ needs.container-build-push.result }}" == "success" && "${{ needs.create-manifest.result }}" == "success" ]]; then
echo "outcome=success" >> $GITHUB_OUTPUT
else
echo "outcome=failure" >> $GITHUB_OUTPUT
fi
- name: Notify container push completed
uses: ./.github/actions/slack-notification
env:
SLACK_CHANNEL_ID: ${{ secrets.SLACK_PLATFORM_DEPLOYMENTS }}
MESSAGE_TS: ${{ needs.notify-release-started.outputs.message-ts }}
COMPONENT: SDK
RELEASE_TAG: ${{ needs.setup.outputs.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-completed.json"
step-outcome: ${{ steps.outcome.outputs.outcome }}
update-ts: ${{ needs.notify-release-started.outputs.message-ts }}
dispatch-v3-deployment:
if: needs.setup.outputs.prowler_version_major == '3'
needs: [setup, container-build-push]
if: needs.container-build-push.outputs.prowler_version_major == '3'
needs: container-build-push
runs-on: ubuntu-latest
timeout-minutes: 5
permissions:
@@ -308,4 +282,4 @@ jobs:
token: ${{ secrets.PROWLER_BOT_ACCESS_TOKEN }}
repository: ${{ secrets.DISPATCH_OWNER }}/${{ secrets.DISPATCH_REPO }}
event-type: dispatch
client-payload: '{"version":"release","tag":"${{ needs.setup.outputs.prowler_version }}"}'
client-payload: '{"version":"release","tag":"${{ needs.container-build-push.outputs.prowler_version }}"}'
+1 -102
View File
@@ -82,110 +82,9 @@ jobs:
./tests/**/aws/**
./poetry.lock
- name: Resolve AWS services under test
if: steps.changed-aws.outputs.any_changed == 'true'
id: aws-services
shell: bash
run: |
python3 <<'PY'
import os
from pathlib import Path
dependents = {
"acm": ["elb"],
"autoscaling": ["dynamodb"],
"awslambda": ["ec2", "inspector2"],
"backup": ["dynamodb", "ec2", "rds"],
"cloudfront": ["shield"],
"cloudtrail": ["awslambda", "cloudwatch"],
"cloudwatch": ["bedrock"],
"ec2": ["dlm", "dms", "elbv2", "emr", "inspector2", "rds", "redshift", "route53", "shield", "ssm"],
"ecr": ["inspector2"],
"elb": ["shield"],
"elbv2": ["shield"],
"globalaccelerator": ["shield"],
"iam": ["bedrock", "cloudtrail", "cloudwatch", "codebuild"],
"kafka": ["firehose"],
"kinesis": ["firehose"],
"kms": ["kafka"],
"organizations": ["iam", "servicecatalog"],
"route53": ["shield"],
"s3": ["bedrock", "cloudfront", "cloudtrail", "macie"],
"ssm": ["ec2"],
"vpc": ["awslambda", "ec2", "efs", "elasticache", "neptune", "networkfirewall", "rds", "redshift", "workspaces"],
"waf": ["elbv2"],
"wafv2": ["cognito", "elbv2"],
}
changed_raw = """${{ steps.changed-aws.outputs.all_changed_files }}"""
# all_changed_files is space-separated, not newline-separated
# Strip leading "./" if present for consistent path handling
changed_files = [Path(f.lstrip("./")) for f in changed_raw.split() if f]
services = set()
run_all = False
for path in changed_files:
path_str = path.as_posix()
parts = path.parts
if path_str.startswith("prowler/providers/aws/services/"):
if len(parts) > 4 and "." not in parts[4]:
services.add(parts[4])
else:
run_all = True
elif path_str.startswith("tests/providers/aws/services/"):
if len(parts) > 4 and "." not in parts[4]:
services.add(parts[4])
else:
run_all = True
elif path_str.startswith("prowler/providers/aws/") or path_str.startswith("tests/providers/aws/"):
run_all = True
# Expand with direct dependent services (one level only)
# We only test services that directly depend on the changed services,
# not transitive dependencies (services that depend on dependents)
original_services = set(services)
for svc in original_services:
for dep in dependents.get(svc, []):
services.add(dep)
if run_all or not services:
run_all = True
services = set()
service_paths = " ".join(sorted(f"tests/providers/aws/services/{svc}" for svc in services))
output_lines = [
f"run_all={'true' if run_all else 'false'}",
f"services={' '.join(sorted(services))}",
f"service_paths={service_paths}",
]
with open(os.environ["GITHUB_OUTPUT"], "a") as gh_out:
for line in output_lines:
gh_out.write(line + "\n")
print(f"AWS changed files (filtered): {changed_raw or 'none'}")
print(f"Run all AWS tests: {run_all}")
if services:
print(f"AWS service test paths: {service_paths}")
else:
print("AWS service test paths: none detected")
PY
- name: Run AWS tests
if: steps.changed-aws.outputs.any_changed == 'true'
run: |
echo "AWS run_all=${{ steps.aws-services.outputs.run_all }}"
echo "AWS service_paths='${{ steps.aws-services.outputs.service_paths }}'"
if [ "${{ steps.aws-services.outputs.run_all }}" = "true" ]; then
poetry run pytest -n auto --cov=./prowler/providers/aws --cov-report=xml:aws_coverage.xml tests/providers/aws
elif [ -z "${{ steps.aws-services.outputs.service_paths }}" ]; then
echo "No AWS service paths detected; skipping AWS tests."
else
poetry run pytest -n auto --cov=./prowler/providers/aws --cov-report=xml:aws_coverage.xml ${{ steps.aws-services.outputs.service_paths }}
fi
run: poetry run pytest -n auto --cov=./prowler/providers/aws --cov-report=xml:aws_coverage.xml tests/providers/aws
- name: Upload AWS coverage to Codecov
if: steps.changed-aws.outputs.any_changed == 'true'
+33 -61
View File
@@ -50,34 +50,8 @@ jobs:
id: set-short-sha
run: echo "short-sha=${GITHUB_SHA::7}" >> $GITHUB_OUTPUT
notify-release-started:
if: github.repository == 'prowler-cloud/prowler' && (github.event_name == 'release' || github.event_name == 'workflow_dispatch')
needs: setup
runs-on: ubuntu-latest
timeout-minutes: 5
outputs:
message-ts: ${{ steps.slack-notification.outputs.ts }}
steps:
- name: Checkout repository
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Notify container push started
id: slack-notification
uses: ./.github/actions/slack-notification
env:
SLACK_CHANNEL_ID: ${{ secrets.SLACK_PLATFORM_DEPLOYMENTS }}
COMPONENT: UI
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"
container-build-push:
needs: [setup, notify-release-started]
if: always() && needs.setup.result == 'success' && (needs.notify-release-started.result == 'success' || needs.notify-release-started.result == 'skipped')
needs: setup
runs-on: ${{ matrix.runner }}
strategy:
matrix:
@@ -106,6 +80,21 @@ jobs:
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1
- name: Notify container push started
id: slack-notification-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: UI
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 UI container for ${{ matrix.arch }}
id: container-push
if: github.event_name == 'push' || github.event_name == 'release' || github.event_name == 'workflow_dispatch'
@@ -122,6 +111,23 @@ jobs:
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()
uses: ./.github/actions/slack-notification
env:
SLACK_CHANNEL_ID: ${{ secrets.SLACK_PLATFORM_DEPLOYMENTS }}
MESSAGE_TS: ${{ steps.slack-notification-started.outputs.ts }}
COMPONENT: UI
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-completed.json"
step-outcome: ${{ steps.container-push.outcome }}
update-ts: ${{ steps.slack-notification-started.outputs.ts }}
# Create and push multi-architecture manifest
create-manifest:
needs: [setup, container-build-push]
@@ -168,40 +174,6 @@ jobs:
regctl tag delete "${{ env.PROWLERCLOUD_DOCKERHUB_REPOSITORY }}/${{ env.PROWLERCLOUD_DOCKERHUB_IMAGE }}:${{ needs.setup.outputs.short-sha }}-arm64" || true
echo "Cleanup completed"
notify-release-completed:
if: always() && needs.notify-release-started.result == 'success' && (github.event_name == 'release' || github.event_name == 'workflow_dispatch')
needs: [setup, notify-release-started, container-build-push, create-manifest]
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- name: Checkout repository
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Determine overall outcome
id: outcome
run: |
if [[ "${{ needs.container-build-push.result }}" == "success" && "${{ needs.create-manifest.result }}" == "success" ]]; then
echo "outcome=success" >> $GITHUB_OUTPUT
else
echo "outcome=failure" >> $GITHUB_OUTPUT
fi
- name: Notify container push completed
uses: ./.github/actions/slack-notification
env:
SLACK_CHANNEL_ID: ${{ secrets.SLACK_PLATFORM_DEPLOYMENTS }}
MESSAGE_TS: ${{ needs.notify-release-started.outputs.message-ts }}
COMPONENT: UI
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-completed.json"
step-outcome: ${{ steps.outcome.outputs.outcome }}
update-ts: ${{ needs.notify-release-started.outputs.message-ts }}
trigger-deployment:
if: github.event_name == 'push'
needs: [setup, container-build-push]
+18 -22
View File
@@ -6,7 +6,7 @@
<b><i>Prowler</b> is the Open Cloud Security platform trusted by thousands to automate security and compliance in any cloud environment. With hundreds of ready-to-use checks and compliance frameworks, Prowler delivers real-time, customizable monitoring and seamless integrations, making cloud security simple, scalable, and cost-effective for organizations of any size.
</p>
<p align="center">
<b>Secure ANY cloud at AI Speed at <a href="https://prowler.com">prowler.com</i></b>
<b>Learn more at <a href="https://prowler.com">prowler.com</i></b>
</p>
<p align="center">
@@ -35,32 +35,28 @@
</p>
<hr>
<p align="center">
<img align="center" src="/docs/img/prowler-cloud.gif" width="100%" height="100%">
<img align="center" src="/docs/img/prowler-cli-quick.gif" width="100%" height="100%">
</p>
# Description
**Prowler** is the worlds most widely used _open-source cloud security platform_ that automates security and compliance across **any cloud environment**. With hundreds of ready-to-use security checks, remediation guidance, and compliance frameworks, Prowler is built to _“Secure ANY cloud at AI Speed”_. Prowler delivers **AI-driven**, **customizable**, and **easy-to-use** assessments, dashboards, reports, and integrations, making cloud security **simple**, **scalable**, and **cost-effective** for organizations of any size.
**Prowler** is an open-source security tool designed to assess and enforce security best practices across AWS, Azure, Google Cloud, and Kubernetes. It supports tasks such as security audits, incident response, continuous monitoring, system hardening, forensic readiness, and remediation processes.
Prowler includes hundreds of built-in controls to ensure compliance with standards and frameworks, including:
- **Prowler ThreatScore:** Weighted risk prioritization scoring that helps you focus on the most critical security findings first
- **Industry Standards:** CIS, NIST 800, NIST CSF, CISA, and MITRE ATT&CK
- **Regulatory Compliance and Governance:** RBI, FedRAMP, PCI-DSS, and NIS2
- **Industry Standards:** CIS, NIST 800, NIST CSF, and CISA
- **Regulatory Compliance and Governance:** RBI, FedRAMP, and PCI-DSS
- **Frameworks for Sensitive Data and Privacy:** GDPR, HIPAA, and FFIEC
- **Frameworks for Organizational Governance and Quality Control:** SOC2, GXP, and ISO 27001
- **Cloud-Specific Frameworks:** AWS Foundational Technical Review (FTR), AWS Well-Architected Framework, and BSI C5
- **National Security Standards:** ENS (Spanish National Security Scheme) and KISA ISMS-P (Korean)
- **Frameworks for Organizational Governance and Quality Control:** SOC2 and GXP
- **AWS-Specific Frameworks:** AWS Foundational Technical Review (FTR) and AWS Well-Architected Framework (Security Pillar)
- **National Security Standards:** ENS (Spanish National Security Scheme)
- **Custom Security Frameworks:** Tailored to your needs
## Prowler App / Prowler Cloud
## Prowler App
Prowler App / [Prowler Cloud](https://cloud.prowler.com/) is a web-based application that simplifies running Prowler across your cloud provider accounts. It provides a user-friendly interface to visualize the results and streamline your security assessments.
Prowler App is a web-based application that simplifies running Prowler across your cloud provider accounts. It provides a user-friendly interface to visualize the results and streamline your security assessments.
![Prowler App](docs/images/products/overview.png)
![Risk Pipeline](docs/images/products/risk-pipeline.png)
![Threat Map](docs/images/products/threat-map.png)
>For more details, refer to the [Prowler App Documentation](https://docs.prowler.com/projects/prowler-open-source/en/latest/#prowler-app-installation)
@@ -86,16 +82,16 @@ prowler dashboard
| Provider | Checks | Services | [Compliance Frameworks](https://docs.prowler.com/projects/prowler-open-source/en/latest/tutorials/compliance/) | [Categories](https://docs.prowler.com/projects/prowler-open-source/en/latest/tutorials/misc/#categories) | Support | Interface |
|---|---|---|---|---|---|---|
| AWS | 584 | 85 | 40 | 17 | Official | UI, API, CLI |
| GCP | 89 | 17 | 14 | 5 | Official | UI, API, CLI |
| Azure | 169 | 22 | 15 | 8 | Official | UI, API, CLI |
| Kubernetes | 84 | 7 | 6 | 9 | Official | UI, API, CLI |
| GitHub | 20 | 2 | 1 | 2 | Official | UI, API, CLI |
| AWS | 576 | 82 | 39 | 10 | Official | UI, API, CLI |
| GCP | 79 | 13 | 13 | 3 | Official | UI, API, CLI |
| Azure | 162 | 19 | 13 | 4 | Official | UI, API, CLI |
| Kubernetes | 83 | 7 | 5 | 7 | Official | UI, API, CLI |
| GitHub | 17 | 2 | 1 | 0 | Official | Stable | UI, API, CLI |
| M365 | 70 | 7 | 3 | 2 | Official | UI, API, CLI |
| OCI | 52 | 15 | 1 | 12 | Official | UI, API, CLI |
| Alibaba Cloud | 63 | 10 | 1 | 9 | Official | CLI |
| OCI | 51 | 13 | 1 | 10 | Official | UI, API, CLI |
| Alibaba Cloud | 61 | 9 | 1 | 9 | Official | CLI |
| IaC | [See `trivy` docs.](https://trivy.dev/latest/docs/coverage/iac/) | N/A | N/A | N/A | Official | UI, API, CLI |
| MongoDB Atlas | 10 | 4 | 0 | 3 | Official | UI, API, CLI |
| MongoDB Atlas | 10 | 3 | 0 | 0 | Official | UI, API, CLI |
| LLM | [See `promptfoo` docs.](https://www.promptfoo.dev/docs/red-team/plugins/) | N/A | N/A | N/A | Official | CLI |
| NHN | 6 | 2 | 1 | 0 | Unofficial | CLI |
+1 -1
View File
@@ -2,7 +2,7 @@
All notable changes to the **Prowler API** are documented in this file.
## [1.16.0] (Prowler v5.15.0)
## [1.16.0] (Unreleased)
### Added
- New endpoint to retrieve an overview of the attack surfaces [(#9309)](https://github.com/prowler-cloud/prowler/pull/9309)
+4 -787
View File
@@ -12,18 +12,6 @@ files = [
{file = "about_time-4.2.1-py3-none-any.whl", hash = "sha256:8bbf4c75fe13cbd3d72f49a03b02c5c7dca32169b6d49117c257e7eb3eaee341"},
]
[[package]]
name = "aiofiles"
version = "24.1.0"
description = "File support for asyncio."
optional = false
python-versions = ">=3.8"
groups = ["main"]
files = [
{file = "aiofiles-24.1.0-py3-none-any.whl", hash = "sha256:b4ec55f4195e3eb5d7abd1bf7e061763e864dd4954231fb8539a0ef8bb8260e5"},
{file = "aiofiles-24.1.0.tar.gz", hash = "sha256:22a075c9e5a3810f0c2e48f3008c94d68c65d763b9b03857924c99e57355166c"},
]
[[package]]
name = "aiohappyeyeballs"
version = "2.6.1"
@@ -160,480 +148,6 @@ files = [
frozenlist = ">=1.1.0"
typing-extensions = {version = ">=4.2", markers = "python_version < \"3.13\""}
[[package]]
name = "alibabacloud-actiontrail20200706"
version = "2.4.1"
description = "Alibaba Cloud ActionTrail (20200706) SDK Library for Python"
optional = false
python-versions = ">=3.6"
groups = ["main"]
files = [
{file = "alibabacloud_actiontrail20200706-2.4.1-py3-none-any.whl", hash = "sha256:5dee0009db9b7cba182fbac742820f6a949287a8faafb843b5107f7dc89136da"},
{file = "alibabacloud_actiontrail20200706-2.4.1.tar.gz", hash = "sha256:b65c6b37a96443fbe625dd5a4dd1be52a7476006a411db75206908b11588ffa8"},
]
[package.dependencies]
alibabacloud-endpoint-util = ">=0.0.4,<1.0.0"
alibabacloud-openapi-util = ">=0.2.2,<1.0.0"
alibabacloud-tea-openapi = ">=0.3.16,<1.0.0"
alibabacloud-tea-util = ">=0.3.13,<1.0.0"
[[package]]
name = "alibabacloud-credentials"
version = "1.0.3"
description = "The alibabacloud credentials module of alibabaCloud Python SDK."
optional = false
python-versions = ">=3.7"
groups = ["main"]
files = [
{file = "alibabacloud-credentials-1.0.3.tar.gz", hash = "sha256:9d8707e96afc6f348e23f5677ed15a21c2dfce7cfe6669776548ee4c80e1dfaf"},
{file = "alibabacloud_credentials-1.0.3-py3-none-any.whl", hash = "sha256:30c8302f204b663c655d97e1c283ee9f9f84a6257d7901b931477d6cf34445a8"},
]
[package.dependencies]
aiofiles = ">=22.1.0,<25.0.0"
alibabacloud-credentials-api = ">=1.0.0,<2.0.0"
alibabacloud-tea = ">=0.4.0"
APScheduler = ">=3.10.0,<4.0.0"
[[package]]
name = "alibabacloud-credentials-api"
version = "1.0.0"
description = "Alibaba Cloud Gateway SPI SDK Library for Python"
optional = false
python-versions = ">=3.7"
groups = ["main"]
files = [
{file = "alibabacloud-credentials-api-1.0.0.tar.gz", hash = "sha256:8c340038d904f0218d7214a8f4088c31912bfcf279af2cbc7d9be4897a97dd2f"},
]
[[package]]
name = "alibabacloud-cs20151215"
version = "6.1.0"
description = "Alibaba Cloud CS (20151215) SDK Library for Python"
optional = false
python-versions = ">=3.6"
groups = ["main"]
files = [
{file = "alibabacloud_cs20151215-6.1.0-py3-none-any.whl", hash = "sha256:75e90b1bb9acca2236244bb0e44234ca4805d456ea4303ba4225ac15152a458e"},
{file = "alibabacloud_cs20151215-6.1.0.tar.gz", hash = "sha256:5b3d99306701bf499ddd57cd9f2905b7721cb1bb4bb38ffe4d051f7b4e80e355"},
]
[package.dependencies]
alibabacloud-endpoint-util = ">=0.0.4,<1.0.0"
alibabacloud-openapi-util = ">=0.2.2,<1.0.0"
alibabacloud-tea-openapi = ">=0.3.16,<1.0.0"
alibabacloud-tea-util = ">=0.3.13,<1.0.0"
[[package]]
name = "alibabacloud-darabonba-array"
version = "0.1.0"
description = "Alibaba Cloud Darabonba Array SDK Library for Python"
optional = false
python-versions = ">=3.6"
groups = ["main"]
files = [
{file = "alibabacloud_darabonba_array-0.1.0.tar.gz", hash = "sha256:7f9a7c632518ff4f0cebb0d4e825a48c12e7cf0b9016ea25054dd73732e155aa"},
]
[[package]]
name = "alibabacloud-darabonba-encode-util"
version = "0.0.2"
description = "Darabonba Util Library for Alibaba Cloud Python SDK"
optional = false
python-versions = ">=3.6"
groups = ["main"]
files = [
{file = "alibabacloud_darabonba_encode_util-0.0.2.tar.gz", hash = "sha256:f1c484f276d60450fa49b4b2987194e741fcb2f7faae7f287c0ae65abc85fd4d"},
]
[[package]]
name = "alibabacloud-darabonba-map"
version = "0.0.1"
description = "Alibaba Cloud Darabonba Map SDK Library for Python"
optional = false
python-versions = ">=3.6"
groups = ["main"]
files = [
{file = "alibabacloud_darabonba_map-0.0.1.tar.gz", hash = "sha256:adb17384658a1a8f72418f1838d4b6a5fd2566bfd392a3ef06d9dbb0a595a23f"},
]
[[package]]
name = "alibabacloud-darabonba-signature-util"
version = "0.0.4"
description = "Darabonba Util Library for Alibaba Cloud Python SDK"
optional = false
python-versions = ">=3.6"
groups = ["main"]
files = [
{file = "alibabacloud_darabonba_signature_util-0.0.4.tar.gz", hash = "sha256:71d79b2ae65957bcfbf699ced894fda782b32f9635f1616635533e5a90d5feb0"},
]
[package.dependencies]
cryptography = ">=3.0.0"
[[package]]
name = "alibabacloud-darabonba-string"
version = "0.0.4"
description = "Alibaba Cloud Darabonba String Library for Python"
optional = false
python-versions = "*"
groups = ["main"]
files = [
{file = "alibabacloud-darabonba-string-0.0.4.tar.gz", hash = "sha256:ec6614c0448dadcbc5e466485838a1f8cfdd911135bea739e20b14511270c6f7"},
]
[[package]]
name = "alibabacloud-darabonba-time"
version = "0.0.1"
description = "Alibaba Cloud Darabonba Time SDK Library for Python"
optional = false
python-versions = ">=3.6"
groups = ["main"]
files = [
{file = "alibabacloud_darabonba_time-0.0.1.tar.gz", hash = "sha256:0ad9c7b0696570d1a3f40106cc7777f755fd92baa0d1dcab5b7df78dde5b922d"},
]
[[package]]
name = "alibabacloud-ecs20140526"
version = "7.2.5"
description = "Alibaba Cloud Elastic Compute Service (20140526) SDK Library for Python"
optional = false
python-versions = ">=3.6"
groups = ["main"]
files = [
{file = "alibabacloud_ecs20140526-7.2.5-py3-none-any.whl", hash = "sha256:10bda5e185f6ba899e7d51477373595c629d66db7530a8a37433fb4e9034a96f"},
{file = "alibabacloud_ecs20140526-7.2.5.tar.gz", hash = "sha256:2abbe630ce42d69061821f38950b938c5982cc31902ccd7132d05be328765a55"},
]
[package.dependencies]
alibabacloud-endpoint-util = ">=0.0.4,<1.0.0"
alibabacloud-openapi-util = ">=0.2.2,<1.0.0"
alibabacloud-tea-openapi = ">=0.3.16,<1.0.0"
alibabacloud-tea-util = ">=0.3.13,<1.0.0"
[[package]]
name = "alibabacloud-endpoint-util"
version = "0.0.4"
description = "The endpoint-util module of alibabaCloud Python SDK."
optional = false
python-versions = "*"
groups = ["main"]
files = [
{file = "alibabacloud_endpoint_util-0.0.4.tar.gz", hash = "sha256:a593eb8ddd8168d5dc2216cd33111b144f9189fcd6e9ca20e48f358a739bbf90"},
]
[[package]]
name = "alibabacloud-gateway-oss"
version = "0.0.17"
description = "Alibaba Cloud OSS SDK Library for Python"
optional = false
python-versions = ">=3.6"
groups = ["main"]
files = [
{file = "alibabacloud_gateway_oss-0.0.17.tar.gz", hash = "sha256:8c4b66c8c7dd285fc210ee232ab3f062b5573258752804d19382000746531e29"},
]
[package.dependencies]
alibabacloud_credentials = ">=0.3.5"
alibabacloud_darabonba_array = ">=0.1.0,<1.0.0"
alibabacloud_darabonba_encode_util = ">=0.0.2,<1.0.0"
alibabacloud_darabonba_map = ">=0.0.1,<1.0.0"
alibabacloud_darabonba_signature_util = ">=0.0.4,<1.0.0"
alibabacloud_darabonba_string = ">=0.0.4,<1.0.0"
alibabacloud_darabonba_time = ">=0.0.1,<1.0.0"
alibabacloud_gateway_oss_util = ">=0.0.3,<1.0.0"
alibabacloud_gateway_spi = ">=0.0.1,<1.0.0"
alibabacloud_openapi_util = ">=0.2.1,<1.0.0"
alibabacloud_oss_util = ">=0.0.5,<1.0.0"
alibabacloud_tea_util = ">=0.3.11,<1.0.0"
alibabacloud_tea_xml = ">=0.0.2,<1.0.0"
[[package]]
name = "alibabacloud-gateway-oss-util"
version = "0.0.3"
description = "Alibaba Cloud OSS Util Library for Python"
optional = false
python-versions = ">=3.6"
groups = ["main"]
files = [
{file = "alibabacloud_gateway_oss_util-0.0.3.tar.gz", hash = "sha256:5eb7fa450dc7350d5c71577974b9d7f489479e5c5ec7efc1c5376385e8c1c0a5"},
]
[[package]]
name = "alibabacloud-gateway-sls"
version = "0.4.0"
description = "Alibaba Cloud SLS Gateway Library for Python"
optional = false
python-versions = ">=3.7"
groups = ["main"]
files = [
{file = "alibabacloud_gateway_sls-0.4.0-py3-none-any.whl", hash = "sha256:a0299a83a5528025983b42b7533a28028461bced5e180a66f97999e0134760a6"},
{file = "alibabacloud_gateway_sls-0.4.0.tar.gz", hash = "sha256:9d2aceb377c9b3ed0558149fda16fe39fa114cc0a22e22a88dc76efdda34633b"},
]
[package.dependencies]
alibabacloud-credentials = ">=1.0.2,<2.0.0"
alibabacloud-darabonba-array = ">=0.1.0,<1.0.0"
alibabacloud-darabonba-encode-util = ">=0.0.2,<1.0.0"
alibabacloud-darabonba-map = ">=0.0.1,<1.0.0"
alibabacloud-darabonba-signature-util = ">=0.0.4,<1.0.0"
alibabacloud-darabonba-string = ">=0.0.4,<1.0.0"
alibabacloud-gateway-sls-util = ">=0.4.0,<1.0.0"
alibabacloud-gateway-spi = ">=0.0.2,<1.0.0"
alibabacloud-openapi-util = ">=0.2.2,<1.0.0"
alibabacloud-tea-util = ">=0.3.13,<1.0.0"
[[package]]
name = "alibabacloud-gateway-sls-util"
version = "0.4.0"
description = "Alibaba Cloud SLS Util Library for Python"
optional = false
python-versions = ">=3.7"
groups = ["main"]
files = [
{file = "alibabacloud_gateway_sls_util-0.4.0-py3-none-any.whl", hash = "sha256:c91ab7fe55af526a01d25b0d431088c4d241b160db055da3d8cb7330bd74595a"},
{file = "alibabacloud_gateway_sls_util-0.4.0.tar.gz", hash = "sha256:f8b683a36a2ae3fe9a8225d3d97773ea769bdf9cdf4f4d033eab2eb6062ddd1f"},
]
[package.dependencies]
aliyun-log-fastpb = ">=0.2.0"
lz4 = ">=4.3.2"
zstd = ">=1.5.5.1"
[[package]]
name = "alibabacloud-gateway-spi"
version = "0.0.3"
description = "Alibaba Cloud Gateway SPI SDK Library for Python"
optional = false
python-versions = ">=3.6"
groups = ["main"]
files = [
{file = "alibabacloud_gateway_spi-0.0.3.tar.gz", hash = "sha256:10d1c53a3fc5f87915fbd6b4985b98338a776e9b44a0263f56643c5048223b8b"},
]
[package.dependencies]
alibabacloud_credentials = ">=0.3.4"
[[package]]
name = "alibabacloud-openapi-util"
version = "0.2.2"
description = "Aliyun Tea OpenApi Library for Python"
optional = false
python-versions = "*"
groups = ["main"]
files = [
{file = "alibabacloud_openapi_util-0.2.2.tar.gz", hash = "sha256:ebbc3906f554cb4bf8f513e43e8a33e8b6a3d4a0ef13617a0e14c3dda8ef52a8"},
]
[package.dependencies]
alibabacloud_tea_util = ">=0.0.2"
cryptography = ">=3.0.0"
[[package]]
name = "alibabacloud-oss-util"
version = "0.0.6"
description = "The oss util module of alibabaCloud Python SDK."
optional = false
python-versions = "*"
groups = ["main"]
files = [
{file = "alibabacloud_oss_util-0.0.6.tar.gz", hash = "sha256:d3ecec36632434bd509a113e8cf327dc23e830ac8d9dd6949926f4e334c8b5d6"},
]
[package.dependencies]
alibabacloud-tea = "*"
[[package]]
name = "alibabacloud-oss20190517"
version = "1.0.6"
description = "Alibaba Cloud Object Storage Service (20190517) SDK Library for Python"
optional = false
python-versions = ">=3.6"
groups = ["main"]
files = [
{file = "alibabacloud_oss20190517-1.0.6-py3-none-any.whl", hash = "sha256:365fda353de6658a1a289f4d70dcd0394e2a8e2921b6b5834ba6d9772121d2f6"},
{file = "alibabacloud_oss20190517-1.0.6.tar.gz", hash = "sha256:7cd0fb16af613ceb38d2e0e529aa1f58038c7cf59eb67c8c8775ae44ea717852"},
]
[package.dependencies]
alibabacloud-gateway-oss = ">=0.0.9,<1.0.0"
alibabacloud-gateway-spi = ">=0.0.1,<1.0.0"
alibabacloud-openapi-util = ">=0.2.1,<1.0.0"
alibabacloud-tea-openapi = ">=0.3.6,<1.0.0"
alibabacloud-tea-util = ">=0.3.11,<1.0.0"
[[package]]
name = "alibabacloud-ram20150501"
version = "1.2.0"
description = "Alibaba Cloud Resource Access Management (20150501) SDK Library for Python"
optional = false
python-versions = ">=3.6"
groups = ["main"]
files = [
{file = "alibabacloud_ram20150501-1.2.0-py3-none-any.whl", hash = "sha256:03a0f2a0259848787c1f74e802b486184a88e04183486bd9398766971e5eb00a"},
{file = "alibabacloud_ram20150501-1.2.0.tar.gz", hash = "sha256:6253513c8880769f4fd5b36fedddb362a9ca628ad9ae9c05c0eeacf5fbc95b42"},
]
[package.dependencies]
alibabacloud-endpoint-util = ">=0.0.4,<1.0.0"
alibabacloud-openapi-util = ">=0.2.2,<1.0.0"
alibabacloud-tea-openapi = ">=0.3.15,<1.0.0"
alibabacloud-tea-util = ">=0.3.13,<1.0.0"
[[package]]
name = "alibabacloud-rds20140815"
version = "12.0.0"
description = "Alibaba Cloud rds (20140815) SDK Library for Python"
optional = false
python-versions = ">=3.6"
groups = ["main"]
files = [
{file = "alibabacloud_rds20140815-12.0.0-py3-none-any.whl", hash = "sha256:0bd7e2018a428d86b1b0681087336e74665b48fc3eb0a13c4f4377ed5eab2b08"},
{file = "alibabacloud_rds20140815-12.0.0.tar.gz", hash = "sha256:e7421d94f18a914c0a06b0e7fad0daff557713f1c97d415d463a78c1270e9b98"},
]
[package.dependencies]
alibabacloud-endpoint-util = ">=0.0.4,<1.0.0"
alibabacloud-openapi-util = ">=0.2.2,<1.0.0"
alibabacloud-tea-openapi = ">=0.3.15,<1.0.0"
alibabacloud-tea-util = ">=0.3.13,<1.0.0"
[[package]]
name = "alibabacloud-sas20181203"
version = "6.1.0"
description = "Alibaba Cloud Threat Detection (20181203) SDK Library for Python"
optional = false
python-versions = ">=3.6"
groups = ["main"]
files = [
{file = "alibabacloud_sas20181203-6.1.0-py3-none-any.whl", hash = "sha256:1ad735332c50c7961be036b17420d56b5ec3b5557e3aea1daa19491e8b75da20"},
{file = "alibabacloud_sas20181203-6.1.0.tar.gz", hash = "sha256:e49ffd53e630274a8bf5a8299ca753023ad118510c80f6d9c6fb018b7479bf37"},
]
[package.dependencies]
alibabacloud-endpoint-util = ">=0.0.4,<1.0.0"
alibabacloud-openapi-util = ">=0.2.2,<1.0.0"
alibabacloud-tea-openapi = ">=0.3.16,<1.0.0"
alibabacloud-tea-util = ">=0.3.13,<1.0.0"
[[package]]
name = "alibabacloud-sls20201230"
version = "5.9.0"
description = "Alibaba Cloud Log Service (20201230) SDK Library for Python"
optional = false
python-versions = ">=3.6"
groups = ["main"]
files = [
{file = "alibabacloud_sls20201230-5.9.0-py3-none-any.whl", hash = "sha256:c4ae14096817a9686af5a0ae2389f1f6a8781e60b9edb8643445250cf15c26f1"},
{file = "alibabacloud_sls20201230-5.9.0.tar.gz", hash = "sha256:bea830b64fbc7ed1719ba386ceeefb120f08d705f03eb0e02409dc6f12a291da"},
]
[package.dependencies]
alibabacloud-gateway-sls = ">=0.3.0,<1.0.0"
alibabacloud-openapi-util = ">=0.2.2,<1.0.0"
alibabacloud-tea-openapi = ">=0.3.16,<1.0.0"
alibabacloud-tea-util = ">=0.3.13,<1.0.0"
[[package]]
name = "alibabacloud-sts20150401"
version = "1.1.6"
description = "Alibaba Cloud Sts (20150401) SDK Library for Python"
optional = false
python-versions = ">=3.6"
groups = ["main"]
files = [
{file = "alibabacloud_sts20150401-1.1.6-py3-none-any.whl", hash = "sha256:627f5ca1f86e19b0bf8ce0e99071a36fb65579fad9256fbee38fdc8d500598e9"},
{file = "alibabacloud_sts20150401-1.1.6.tar.gz", hash = "sha256:c2529b41e0e4531e21cb393e4df346e19fd6d54cc6337d1138dbcd2191438d4c"},
]
[package.dependencies]
alibabacloud-endpoint-util = ">=0.0.4,<1.0.0"
alibabacloud-openapi-util = ">=0.2.2,<1.0.0"
alibabacloud-tea-openapi = ">=0.3.15,<1.0.0"
alibabacloud-tea-util = ">=0.3.13,<1.0.0"
[[package]]
name = "alibabacloud-tea"
version = "0.4.3"
description = "The tea module of alibabaCloud Python SDK."
optional = false
python-versions = ">=3.7"
groups = ["main"]
files = [
{file = "alibabacloud-tea-0.4.3.tar.gz", hash = "sha256:ec8053d0aa8d43ebe1deb632d5c5404339b39ec9a18a0707d57765838418504a"},
]
[package.dependencies]
aiohttp = ">=3.7.0,<4.0.0"
requests = ">=2.21.0,<3.0.0"
[[package]]
name = "alibabacloud-tea-openapi"
version = "0.4.1"
description = "Alibaba Cloud openapi SDK Library for Python"
optional = false
python-versions = ">=3.7"
groups = ["main"]
files = [
{file = "alibabacloud_tea_openapi-0.4.1-py3-none-any.whl", hash = "sha256:e46bfa3ca34086d2c357d217a0b7284ecbd4b3bab5c88e075e73aec637b0e4a0"},
{file = "alibabacloud_tea_openapi-0.4.1.tar.gz", hash = "sha256:2384b090870fdb089c3c40f3fb8cf0145b8c7d6c14abbac521f86a01abb5edaf"},
]
[package.dependencies]
alibabacloud-credentials = ">=1.0.2,<2.0.0"
alibabacloud-gateway-spi = ">=0.0.2,<1.0.0"
alibabacloud-tea-util = ">=0.3.13,<1.0.0"
cryptography = ">=3.0.0,<45.0.0"
darabonba-core = ">=1.0.3,<2.0.0"
[[package]]
name = "alibabacloud-tea-util"
version = "0.3.14"
description = "The tea-util module of alibabaCloud Python SDK."
optional = false
python-versions = ">=3.6"
groups = ["main"]
files = [
{file = "alibabacloud_tea_util-0.3.14-py3-none-any.whl", hash = "sha256:10d3e5c340d8f7ec69dd27345eb2fc5a1dab07875742525edf07bbe86db93bfe"},
{file = "alibabacloud_tea_util-0.3.14.tar.gz", hash = "sha256:708e7c9f64641a3c9e0e566365d2f23675f8d7c2a3e2971d9402ceede0408cdb"},
]
[package.dependencies]
alibabacloud-tea = ">=0.3.3"
[[package]]
name = "alibabacloud-tea-xml"
version = "0.0.3"
description = "The tea-xml module of alibabaCloud Python SDK."
optional = false
python-versions = "*"
groups = ["main"]
files = [
{file = "alibabacloud_tea_xml-0.0.3.tar.gz", hash = "sha256:979cb51fadf43de77f41c69fc69c12529728919f849723eb0cd24eb7b048a90c"},
]
[package.dependencies]
alibabacloud-tea = ">=0.4.0"
[[package]]
name = "alibabacloud-vpc20160428"
version = "6.13.0"
description = "Alibaba Cloud Virtual Private Cloud (20160428) SDK Library for Python"
optional = false
python-versions = ">=3.6"
groups = ["main"]
files = [
{file = "alibabacloud_vpc20160428-6.13.0-py3-none-any.whl", hash = "sha256:933cf1e74322a20a2df27ca6323760d857744a4246eeadc9fb3eae01322fb1c6"},
{file = "alibabacloud_vpc20160428-6.13.0.tar.gz", hash = "sha256:daf00679a83d422799f9fcf263739fe1f360641675843cbfbe623833fc8b1681"},
]
[package.dependencies]
alibabacloud-endpoint-util = ">=0.0.4,<1.0.0"
alibabacloud-openapi-util = ">=0.2.2,<1.0.0"
alibabacloud-tea-openapi = ">=0.3.16,<1.0.0"
alibabacloud-tea-util = ">=0.3.13,<1.0.0"
[[package]]
name = "alive-progress"
version = "3.3.0"
@@ -650,32 +164,6 @@ files = [
about-time = "4.2.1"
graphemeu = "0.7.2"
[[package]]
name = "aliyun-log-fastpb"
version = "0.2.0"
description = "Fast protobuf serialization for Aliyun Log using PyO3 and quick-protobuf"
optional = false
python-versions = ">=3.7"
groups = ["main"]
files = [
{file = "aliyun_log_fastpb-0.2.0-cp37-abi3-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:51633d92d2b349aed4843c0b503454fb4f7d73eeaaa54f82aa5a36c10c064ef5"},
{file = "aliyun_log_fastpb-0.2.0-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:d2984aafc61ccbbf1db2589ce90b6d5a26e72dba137fb1fdf7f61ce3faa967c0"},
{file = "aliyun_log_fastpb-0.2.0-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:181fc61ac9934f58b0880fa5617a4a4dc709dba09f8be95b5a71e828f2e48053"},
{file = "aliyun_log_fastpb-0.2.0-cp37-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:12b8bfddf0bc5450f16f1954c6387a73da124fae10d1205a17a0117e66bb56db"},
{file = "aliyun_log_fastpb-0.2.0-cp37-abi3-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:8fbc83cbaa51d332e5e68871c1200014f1f3de54a8cba4fb55a634ee145cd4e4"},
{file = "aliyun_log_fastpb-0.2.0-cp37-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:42a86a6e11dd227d595fa23f69d30588446af19d045d1003bd1b66b5c9a55485"},
{file = "aliyun_log_fastpb-0.2.0-cp37-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd92c0b84ba300c1d1c227204c5f2fff243cea80bc3f9399293385e87c82ee3e"},
{file = "aliyun_log_fastpb-0.2.0-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d7c07a6d81a3eab6666949240da305236ed2350c305154d7e39fcc121fc52291"},
{file = "aliyun_log_fastpb-0.2.0-cp37-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2cff4fbdd0edff94adcee1dcabf16daacb5d336a12fc897887aa6e4f0ad25152"},
{file = "aliyun_log_fastpb-0.2.0-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:5a451809e2a062accbb8dae8750e507e58806e4a8da48d69215cdeef428e9d63"},
{file = "aliyun_log_fastpb-0.2.0-cp37-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:61f09df30232f1f5628d13310cf0e175171399ea1c75a8470e9f9d97b045bfb5"},
{file = "aliyun_log_fastpb-0.2.0-cp37-abi3-musllinux_1_2_i686.whl", hash = "sha256:a5fbf0d41d8c0c964a3dc8dd0ee2e732f876b803e0ed3432550ef3b84dde84f1"},
{file = "aliyun_log_fastpb-0.2.0-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:ae2f84ed0777e00045791044a56413f370afbd5b061505f5ded540c04b19c58e"},
{file = "aliyun_log_fastpb-0.2.0-cp37-abi3-win32.whl", hash = "sha256:967f9656c805602fd9be07d8c2756ad89204c852c99689c3c71aa035416ef42a"},
{file = "aliyun_log_fastpb-0.2.0-cp37-abi3-win_amd64.whl", hash = "sha256:bbdcf7b85f0f3437c2a8e8a1db0ef5584d21468b7c7a358269a4c651c84f4a54"},
{file = "aliyun_log_fastpb-0.2.0.tar.gz", hash = "sha256:91c714e76fb941c9a0db6b1aa1f4c56cb1626254ff5444c1179860f5e5b63d93"},
]
[[package]]
name = "amqp"
version = "5.3.1"
@@ -723,34 +211,6 @@ typing_extensions = {version = ">=4.5", markers = "python_version < \"3.13\""}
[package.extras]
trio = ["trio (>=0.26.1)"]
[[package]]
name = "apscheduler"
version = "3.11.1"
description = "In-process task scheduler with Cron-like capabilities"
optional = false
python-versions = ">=3.8"
groups = ["main"]
files = [
{file = "apscheduler-3.11.1-py3-none-any.whl", hash = "sha256:6162cb5683cb09923654fa9bdd3130c4be4bfda6ad8990971c9597ecd52965d2"},
{file = "apscheduler-3.11.1.tar.gz", hash = "sha256:0db77af6400c84d1747fe98a04b8b58f0080c77d11d338c4f507a9752880f221"},
]
[package.dependencies]
tzlocal = ">=3.0"
[package.extras]
doc = ["packaging", "sphinx", "sphinx-rtd-theme (>=1.3.0)"]
etcd = ["etcd3", "protobuf (<=3.21.0)"]
gevent = ["gevent"]
mongodb = ["pymongo (>=3.0)"]
redis = ["redis (>=3.0)"]
rethinkdb = ["rethinkdb (>=2.4.0)"]
sqlalchemy = ["sqlalchemy (>=1.4)"]
test = ["APScheduler[etcd,mongodb,redis,rethinkdb,sqlalchemy,tornado,zookeeper]", "PySide6 ; platform_python_implementation == \"CPython\" and python_version < \"3.14\"", "anyio (>=4.5.2)", "gevent ; python_version < \"3.14\"", "pytest", "pytz", "twisted ; python_version < \"3.14\""]
tornado = ["tornado (>=4.3)"]
twisted = ["twisted"]
zookeeper = ["kazoo"]
[[package]]
name = "asgiref"
version = "3.9.1"
@@ -2068,23 +1528,6 @@ files = [
docs = ["ipython", "matplotlib", "numpydoc", "sphinx"]
tests = ["pytest", "pytest-cov", "pytest-xdist"]
[[package]]
name = "darabonba-core"
version = "1.0.4"
description = "The darabonba module of alibabaCloud Python SDK."
optional = false
python-versions = ">=3.7"
groups = ["main"]
files = [
{file = "darabonba_core-1.0.4-py3-none-any.whl", hash = "sha256:4c3bc1d76d5af1087297b6afde8e960ea2f54f93e725e2df8453f0b4bb27dd24"},
{file = "darabonba_core-1.0.4.tar.gz", hash = "sha256:6ede4e9bfd458148bab19ab2331716ae9b5c226ba5f6d221de6f88ee65704137"},
]
[package.dependencies]
aiohttp = ">=3.7.0,<4.0.0"
alibabacloud-tea = "*"
requests = ">=2.21.0,<3.0.0"
[[package]]
name = "dash"
version = "3.1.1"
@@ -4040,78 +3483,6 @@ html5 = ["html5lib"]
htmlsoup = ["BeautifulSoup4"]
source = ["Cython (>=3.0.11,<3.1.0)"]
[[package]]
name = "lz4"
version = "4.4.5"
description = "LZ4 Bindings for Python"
optional = false
python-versions = ">=3.9"
groups = ["main"]
files = [
{file = "lz4-4.4.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d221fa421b389ab2345640a508db57da36947a437dfe31aeddb8d5c7b646c22d"},
{file = "lz4-4.4.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:7dc1e1e2dbd872f8fae529acd5e4839efd0b141eaa8ae7ce835a9fe80fbad89f"},
{file = "lz4-4.4.5-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:e928ec2d84dc8d13285b4a9288fd6246c5cde4f5f935b479f50d986911f085e3"},
{file = "lz4-4.4.5-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:daffa4807ef54b927451208f5f85750c545a4abbff03d740835fc444cd97f758"},
{file = "lz4-4.4.5-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2a2b7504d2dffed3fd19d4085fe1cc30cf221263fd01030819bdd8d2bb101cf1"},
{file = "lz4-4.4.5-cp310-cp310-win32.whl", hash = "sha256:0846e6e78f374156ccf21c631de80967e03cc3c01c373c665789dc0c5431e7fc"},
{file = "lz4-4.4.5-cp310-cp310-win_amd64.whl", hash = "sha256:7c4e7c44b6a31de77d4dc9772b7d2561937c9588a734681f70ec547cfbc51ecd"},
{file = "lz4-4.4.5-cp310-cp310-win_arm64.whl", hash = "sha256:15551280f5656d2206b9b43262799c89b25a25460416ec554075a8dc568e4397"},
{file = "lz4-4.4.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d6da84a26b3aa5da13a62e4b89ab36a396e9327de8cd48b436a3467077f8ccd4"},
{file = "lz4-4.4.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:61d0ee03e6c616f4a8b69987d03d514e8896c8b1b7cc7598ad029e5c6aedfd43"},
{file = "lz4-4.4.5-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:33dd86cea8375d8e5dd001e41f321d0a4b1eb7985f39be1b6a4f466cd480b8a7"},
{file = "lz4-4.4.5-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:609a69c68e7cfcfa9d894dc06be13f2e00761485b62df4e2472f1b66f7b405fb"},
{file = "lz4-4.4.5-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:75419bb1a559af00250b8f1360d508444e80ed4b26d9d40ec5b09fe7875cb989"},
{file = "lz4-4.4.5-cp311-cp311-win32.whl", hash = "sha256:12233624f1bc2cebc414f9efb3113a03e89acce3ab6f72035577bc61b270d24d"},
{file = "lz4-4.4.5-cp311-cp311-win_amd64.whl", hash = "sha256:8a842ead8ca7c0ee2f396ca5d878c4c40439a527ebad2b996b0444f0074ed004"},
{file = "lz4-4.4.5-cp311-cp311-win_arm64.whl", hash = "sha256:83bc23ef65b6ae44f3287c38cbf82c269e2e96a26e560aa551735883388dcc4b"},
{file = "lz4-4.4.5-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:df5aa4cead2044bab83e0ebae56e0944cc7fcc1505c7787e9e1057d6d549897e"},
{file = "lz4-4.4.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6d0bf51e7745484d2092b3a51ae6eb58c3bd3ce0300cf2b2c14f76c536d5697a"},
{file = "lz4-4.4.5-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:7b62f94b523c251cf32aa4ab555f14d39bd1a9df385b72443fd76d7c7fb051f5"},
{file = "lz4-4.4.5-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2c3ea562c3af274264444819ae9b14dbbf1ab070aff214a05e97db6896c7597e"},
{file = "lz4-4.4.5-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:24092635f47538b392c4eaeff14c7270d2c8e806bf4be2a6446a378591c5e69e"},
{file = "lz4-4.4.5-cp312-cp312-win32.whl", hash = "sha256:214e37cfe270948ea7eb777229e211c601a3e0875541c1035ab408fbceaddf50"},
{file = "lz4-4.4.5-cp312-cp312-win_amd64.whl", hash = "sha256:713a777de88a73425cf08eb11f742cd2c98628e79a8673d6a52e3c5f0c116f33"},
{file = "lz4-4.4.5-cp312-cp312-win_arm64.whl", hash = "sha256:a88cbb729cc333334ccfb52f070463c21560fca63afcf636a9f160a55fac3301"},
{file = "lz4-4.4.5-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:6bb05416444fafea170b07181bc70640975ecc2a8c92b3b658c554119519716c"},
{file = "lz4-4.4.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:b424df1076e40d4e884cfcc4c77d815368b7fb9ebcd7e634f937725cd9a8a72a"},
{file = "lz4-4.4.5-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:216ca0c6c90719731c64f41cfbd6f27a736d7e50a10b70fad2a9c9b262ec923d"},
{file = "lz4-4.4.5-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:533298d208b58b651662dd972f52d807d48915176e5b032fb4f8c3b6f5fe535c"},
{file = "lz4-4.4.5-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:451039b609b9a88a934800b5fc6ee401c89ad9c175abf2f4d9f8b2e4ef1afc64"},
{file = "lz4-4.4.5-cp313-cp313-win32.whl", hash = "sha256:a5f197ffa6fc0e93207b0af71b302e0a2f6f29982e5de0fbda61606dd3a55832"},
{file = "lz4-4.4.5-cp313-cp313-win_amd64.whl", hash = "sha256:da68497f78953017deb20edff0dba95641cc86e7423dfadf7c0264e1ac60dc22"},
{file = "lz4-4.4.5-cp313-cp313-win_arm64.whl", hash = "sha256:c1cfa663468a189dab510ab231aad030970593f997746d7a324d40104db0d0a9"},
{file = "lz4-4.4.5-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:67531da3b62f49c939e09d56492baf397175ff39926d0bd5bd2d191ac2bff95f"},
{file = "lz4-4.4.5-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a1acbbba9edbcbb982bc2cac5e7108f0f553aebac1040fbec67a011a45afa1ba"},
{file = "lz4-4.4.5-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:a482eecc0b7829c89b498fda883dbd50e98153a116de612ee7c111c8bcf82d1d"},
{file = "lz4-4.4.5-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e099ddfaa88f59dd8d36c8a3c66bd982b4984edf127eb18e30bb49bdba68ce67"},
{file = "lz4-4.4.5-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a2af2897333b421360fdcce895c6f6281dc3fab018d19d341cf64d043fc8d90d"},
{file = "lz4-4.4.5-cp313-cp313t-win32.whl", hash = "sha256:66c5de72bf4988e1b284ebdd6524c4bead2c507a2d7f172201572bac6f593901"},
{file = "lz4-4.4.5-cp313-cp313t-win_amd64.whl", hash = "sha256:cdd4bdcbaf35056086d910d219106f6a04e1ab0daa40ec0eeef1626c27d0fddb"},
{file = "lz4-4.4.5-cp313-cp313t-win_arm64.whl", hash = "sha256:28ccaeb7c5222454cd5f60fcd152564205bcb801bd80e125949d2dfbadc76bbd"},
{file = "lz4-4.4.5-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:c216b6d5275fc060c6280936bb3bb0e0be6126afb08abccde27eed23dead135f"},
{file = "lz4-4.4.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c8e71b14938082ebaf78144f3b3917ac715f72d14c076f384a4c062df96f9df6"},
{file = "lz4-4.4.5-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:9b5e6abca8df9f9bdc5c3085f33ff32cdc86ed04c65e0355506d46a5ac19b6e9"},
{file = "lz4-4.4.5-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3b84a42da86e8ad8537aabef062e7f661f4a877d1c74d65606c49d835d36d668"},
{file = "lz4-4.4.5-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0bba042ec5a61fa77c7e380351a61cb768277801240249841defd2ff0a10742f"},
{file = "lz4-4.4.5-cp314-cp314-win32.whl", hash = "sha256:bd85d118316b53ed73956435bee1997bd06cc66dd2fa74073e3b1322bd520a67"},
{file = "lz4-4.4.5-cp314-cp314-win_amd64.whl", hash = "sha256:92159782a4502858a21e0079d77cdcaade23e8a5d252ddf46b0652604300d7be"},
{file = "lz4-4.4.5-cp314-cp314-win_arm64.whl", hash = "sha256:d994b87abaa7a88ceb7a37c90f547b8284ff9da694e6afcfaa8568d739faf3f7"},
{file = "lz4-4.4.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:f6538aaaedd091d6e5abdaa19b99e6e82697d67518f114721b5248709b639fad"},
{file = "lz4-4.4.5-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:13254bd78fef50105872989a2dc3418ff09aefc7d0765528adc21646a7288294"},
{file = "lz4-4.4.5-cp39-cp39-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:e64e61f29cf95afb43549063d8433b46352baf0c8a70aa45e2585618fcf59d86"},
{file = "lz4-4.4.5-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ff1b50aeeec64df5603f17984e4b5be6166058dcf8f1e26a3da40d7a0f6ab547"},
{file = "lz4-4.4.5-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1dd4d91d25937c2441b9fc0f4af01704a2d09f30a38c5798bc1d1b5a15ec9581"},
{file = "lz4-4.4.5-cp39-cp39-win32.whl", hash = "sha256:d64141085864918392c3159cdad15b102a620a67975c786777874e1e90ef15ce"},
{file = "lz4-4.4.5-cp39-cp39-win_amd64.whl", hash = "sha256:f32b9e65d70f3684532358255dc053f143835c5f5991e28a5ac4c93ce94b9ea7"},
{file = "lz4-4.4.5-cp39-cp39-win_arm64.whl", hash = "sha256:f9b8bde9909a010c75b3aea58ec3910393b758f3c219beed67063693df854db0"},
{file = "lz4-4.4.5.tar.gz", hash = "sha256:5f0b9e53c1e82e88c10d7c180069363980136b9d7a8306c4dca4f760d60c39f0"},
]
[package.extras]
docs = ["sphinx (>=1.6.0)", "sphinx_bootstrap_theme"]
flake8 = ["flake8"]
tests = ["psutil", "pytest (!=3.3.0)", "pytest-cov"]
[[package]]
name = "markdown"
version = "3.9"
@@ -5409,7 +4780,7 @@ files = [
[[package]]
name = "prowler"
version = "5.15.0"
version = "5.14.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"
@@ -5418,19 +4789,6 @@ files = []
develop = false
[package.dependencies]
alibabacloud_actiontrail20200706 = "2.4.1"
alibabacloud_credentials = "1.0.3"
alibabacloud_cs20151215 = "6.1.0"
alibabacloud_ecs20140526 = "7.2.5"
alibabacloud-gateway-oss-util = "0.0.3"
alibabacloud_oss20190517 = "1.0.6"
alibabacloud_ram20150501 = "1.2.0"
alibabacloud-rds20140815 = "12.0.0"
alibabacloud_sas20181203 = "6.1.0"
alibabacloud-sls20201230 = "5.9.0"
alibabacloud_sts20150401 = "1.1.6"
alibabacloud_tea_openapi = "0.4.1"
alibabacloud_vpc20160428 = "6.13.0"
alive-progress = "3.3.0"
awsipranges = "0.3.3"
azure-identity = "1.21.0"
@@ -5494,8 +4852,8 @@ tzlocal = "5.3.1"
[package.source]
type = "git"
url = "https://github.com/prowler-cloud/prowler.git"
reference = "v5.15"
resolved_reference = "e2f30e0987f5e0f46e90a6c547258bb850e36d78"
reference = "master"
resolved_reference = "de5aba6d4db54eed4c95cb7629443da186c17afd"
[[package]]
name = "psutil"
@@ -6707,7 +6065,6 @@ files = [
{file = "ruamel.yaml.clib-0.2.12-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f66efbc1caa63c088dead1c4170d148eabc9b80d95fb75b6c92ac0aad2437d76"},
{file = "ruamel.yaml.clib-0.2.12-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:22353049ba4181685023b25b5b51a574bce33e7f51c759371a7422dcae5402a6"},
{file = "ruamel.yaml.clib-0.2.12-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:932205970b9f9991b34f55136be327501903f7c66830e9760a8ffb15b07f05cd"},
{file = "ruamel.yaml.clib-0.2.12-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a52d48f4e7bf9005e8f0a89209bf9a73f7190ddf0489eee5eb51377385f59f2a"},
{file = "ruamel.yaml.clib-0.2.12-cp310-cp310-win32.whl", hash = "sha256:3eac5a91891ceb88138c113f9db04f3cebdae277f5d44eaa3651a4f573e6a5da"},
{file = "ruamel.yaml.clib-0.2.12-cp310-cp310-win_amd64.whl", hash = "sha256:ab007f2f5a87bd08ab1499bdf96f3d5c6ad4dcfa364884cb4549aa0154b13a28"},
{file = "ruamel.yaml.clib-0.2.12-cp311-cp311-macosx_13_0_arm64.whl", hash = "sha256:4a6679521a58256a90b0d89e03992c15144c5f3858f40d7c18886023d7943db6"},
@@ -6716,7 +6073,6 @@ files = [
{file = "ruamel.yaml.clib-0.2.12-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:811ea1594b8a0fb466172c384267a4e5e367298af6b228931f273b111f17ef52"},
{file = "ruamel.yaml.clib-0.2.12-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:cf12567a7b565cbf65d438dec6cfbe2917d3c1bdddfce84a9930b7d35ea59642"},
{file = "ruamel.yaml.clib-0.2.12-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:7dd5adc8b930b12c8fc5b99e2d535a09889941aa0d0bd06f4749e9a9397c71d2"},
{file = "ruamel.yaml.clib-0.2.12-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1492a6051dab8d912fc2adeef0e8c72216b24d57bd896ea607cb90bb0c4981d3"},
{file = "ruamel.yaml.clib-0.2.12-cp311-cp311-win32.whl", hash = "sha256:bd0a08f0bab19093c54e18a14a10b4322e1eacc5217056f3c063bd2f59853ce4"},
{file = "ruamel.yaml.clib-0.2.12-cp311-cp311-win_amd64.whl", hash = "sha256:a274fb2cb086c7a3dea4322ec27f4cb5cc4b6298adb583ab0e211a4682f241eb"},
{file = "ruamel.yaml.clib-0.2.12-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:20b0f8dc160ba83b6dcc0e256846e1a02d044e13f7ea74a3d1d56ede4e48c632"},
@@ -6725,7 +6081,6 @@ files = [
{file = "ruamel.yaml.clib-0.2.12-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:749c16fcc4a2b09f28843cda5a193e0283e47454b63ec4b81eaa2242f50e4ccd"},
{file = "ruamel.yaml.clib-0.2.12-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:bf165fef1f223beae7333275156ab2022cffe255dcc51c27f066b4370da81e31"},
{file = "ruamel.yaml.clib-0.2.12-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:32621c177bbf782ca5a18ba4d7af0f1082a3f6e517ac2a18b3974d4edf349680"},
{file = "ruamel.yaml.clib-0.2.12-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:b82a7c94a498853aa0b272fd5bc67f29008da798d4f93a2f9f289feb8426a58d"},
{file = "ruamel.yaml.clib-0.2.12-cp312-cp312-win32.whl", hash = "sha256:e8c4ebfcfd57177b572e2040777b8abc537cdef58a2120e830124946aa9b42c5"},
{file = "ruamel.yaml.clib-0.2.12-cp312-cp312-win_amd64.whl", hash = "sha256:0467c5965282c62203273b838ae77c0d29d7638c8a4e3a1c8bdd3602c10904e4"},
{file = "ruamel.yaml.clib-0.2.12-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:4c8c5d82f50bb53986a5e02d1b3092b03622c02c2eb78e29bec33fd9593bae1a"},
@@ -6734,7 +6089,6 @@ files = [
{file = "ruamel.yaml.clib-0.2.12-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:96777d473c05ee3e5e3c3e999f5d23c6f4ec5b0c38c098b3a5229085f74236c6"},
{file = "ruamel.yaml.clib-0.2.12-cp313-cp313-musllinux_1_1_i686.whl", hash = "sha256:3bc2a80e6420ca8b7d3590791e2dfc709c88ab9152c00eeb511c9875ce5778bf"},
{file = "ruamel.yaml.clib-0.2.12-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:e188d2699864c11c36cdfdada94d781fd5d6b0071cd9c427bceb08ad3d7c70e1"},
{file = "ruamel.yaml.clib-0.2.12-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4f6f3eac23941b32afccc23081e1f50612bdbe4e982012ef4f5797986828cd01"},
{file = "ruamel.yaml.clib-0.2.12-cp313-cp313-win32.whl", hash = "sha256:6442cb36270b3afb1b4951f060eccca1ce49f3d087ca1ca4563a6eb479cb3de6"},
{file = "ruamel.yaml.clib-0.2.12-cp313-cp313-win_amd64.whl", hash = "sha256:e5b8daf27af0b90da7bb903a876477a9e6d7270be6146906b276605997c7e9a3"},
{file = "ruamel.yaml.clib-0.2.12-cp39-cp39-macosx_12_0_arm64.whl", hash = "sha256:fc4b630cd3fa2cf7fce38afa91d7cfe844a9f75d7f0f36393fa98815e911d987"},
@@ -6743,7 +6097,6 @@ files = [
{file = "ruamel.yaml.clib-0.2.12-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e2f1c3765db32be59d18ab3953f43ab62a761327aafc1594a2a1fbe038b8b8a7"},
{file = "ruamel.yaml.clib-0.2.12-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:d85252669dc32f98ebcd5d36768f5d4faeaeaa2d655ac0473be490ecdae3c285"},
{file = "ruamel.yaml.clib-0.2.12-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:e143ada795c341b56de9418c58d028989093ee611aa27ffb9b7f609c00d813ed"},
{file = "ruamel.yaml.clib-0.2.12-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:2c59aa6170b990d8d2719323e628aaf36f3bfbc1c26279c0eeeb24d05d2d11c7"},
{file = "ruamel.yaml.clib-0.2.12-cp39-cp39-win32.whl", hash = "sha256:beffaed67936fbbeffd10966a4eb53c402fafd3d6833770516bf7314bc6ffa12"},
{file = "ruamel.yaml.clib-0.2.12-cp39-cp39-win_amd64.whl", hash = "sha256:040ae85536960525ea62868b642bdb0c2cc6021c9f9d507810c0c604e66f5a7b"},
{file = "ruamel.yaml.clib-0.2.12.tar.gz", hash = "sha256:6c8fbb13ec503f99a91901ab46e0b07ae7941cd527393187039aec586fdfd36f"},
@@ -7708,143 +7061,7 @@ docs = ["Sphinx", "furo", "repoze.sphinx.autointerface"]
test = ["coverage[toml]", "zope.event", "zope.testing"]
testing = ["coverage[toml]", "zope.event", "zope.testing"]
[[package]]
name = "zstd"
version = "1.5.7.2"
description = "ZSTD Bindings for Python"
optional = false
python-versions = "*"
groups = ["main"]
files = [
{file = "zstd-1.5.7.2-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:e17104d0e88367a7571dde4286e233126c8551691ceff11f9ae2e3a3ac1bb483"},
{file = "zstd-1.5.7.2-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:d6ee5dfada4c8fa32f43cc092fcf7d8482da6ad242c22fdf780f7eebd0febcc7"},
{file = "zstd-1.5.7.2-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:ae1100776cb400100e2d2f427b50dc983c005c38cd59502eb56d2cfea3402ad5"},
{file = "zstd-1.5.7.2-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:489a0ff15caf7640851e63f85b680c4279c99094cd500a29c7ed3ab82505fce0"},
{file = "zstd-1.5.7.2-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:92590cf54318849d492445c885f1a42b9dbb47cdc070659c7cb61df6e8531047"},
{file = "zstd-1.5.7.2-cp27-cp27mu-manylinux_2_4_i686.whl", hash = "sha256:2bc21650f7b9c058a3c4cb503e906fe9cce293941ec1b48bc5d005c3b4422b42"},
{file = "zstd-1.5.7.2-cp27-cp27mu-manylinux_2_4_x86_64.whl", hash = "sha256:7b13e7eef9aa192804d38bf413924d347c6f6c6ac07f5a0c1ae4a6d7b3af70f0"},
{file = "zstd-1.5.7.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d3f14c5c405ea353b68fe105236780494eb67c756ecd346fd295498f5eab6d24"},
{file = "zstd-1.5.7.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:07d2061df22a3efc06453089e6e8b96e58f5bb7a0c4074dcfd0b0ce243ddde72"},
{file = "zstd-1.5.7.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:27e55aa2043ba7d8a08aba0978c652d4d5857338a8188aa84522569f3586c7bb"},
{file = "zstd-1.5.7.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:8e97933addfd71ea9608306f18dc18e7d2a5e64212ba2bb9a4ccb6d714f9f280"},
{file = "zstd-1.5.7.2-cp310-cp310-manylinux_2_4_i686.whl", hash = "sha256:27e2ed58b64001c9ef0a8e028625477f1a6ed4ca949412ff6548544945cc59c2"},
{file = "zstd-1.5.7.2-cp310-cp310-manylinux_2_4_x86_64.whl", hash = "sha256:92f072819fc0c7e8445f51a232c9ad76642027c069d2f36470cdb5e663839cdb"},
{file = "zstd-1.5.7.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_24_i686.whl", hash = "sha256:2a653cdd2c52d60c28e519d44bde8d759f2c1837f0ff8e8e1b0045ca62fcf70e"},
{file = "zstd-1.5.7.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:047803d87d910f4905f48d99aeff1e0539ec2e4f4bf17d077701b5d0b2392a95"},
{file = "zstd-1.5.7.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:0d8c1dc947e5ccea3bd81043080213685faf1d43886c27c51851fabf325f05c0"},
{file = "zstd-1.5.7.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8291d393321fac30604c6bbf40067103fee315aa476647a5eaecf877ee53496f"},
{file = "zstd-1.5.7.2-cp310-cp310-win32.whl", hash = "sha256:6922ceac5f2d60bb57a7875168c8aa442477b83e8951f2206cf1e9be788b0a6e"},
{file = "zstd-1.5.7.2-cp310-cp310-win_amd64.whl", hash = "sha256:346d1e4774d89a77d67fc70d53964bfca57c0abecfd885a4e00f87fd7c71e074"},
{file = "zstd-1.5.7.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f799c1e9900ad77e7a3d994b9b5146d7cfd1cbd1b61c3db53a697bf21ffcc57b"},
{file = "zstd-1.5.7.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1ff4c667f29101566a7b71f06bbd677a63192818396003354131f586383db042"},
{file = "zstd-1.5.7.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:8526a32fa9f67b07fd09e62474e345f8ca1daf3e37a41137643d45bd1bc90773"},
{file = "zstd-1.5.7.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:2cec2472760d48a7a3445beaba509d3f7850e200fed65db15a1a66e315baec6a"},
{file = "zstd-1.5.7.2-cp311-cp311-manylinux_2_4_i686.whl", hash = "sha256:a200c479ee1bb661bc45518e016a1fdc215a1d8f7e4bf6c7de0af254976cfdf6"},
{file = "zstd-1.5.7.2-cp311-cp311-manylinux_2_4_x86_64.whl", hash = "sha256:f5d159e57a13147aa8293c0f14803a75e9039fd8afdf6cf1c8c2289fb4d2333a"},
{file = "zstd-1.5.7.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_24_i686.whl", hash = "sha256:7206934a2bd390080e972a1fed5a897e184dfd71dbb54e978dc11c6b295e1806"},
{file = "zstd-1.5.7.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:7e0027b20f296d1c9a8e85b8436834cf46560240a29d623aa8eaa8911832eb58"},
{file = "zstd-1.5.7.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:d6b17e5581dd1a13437079bd62838d2635db8eb8aca9c0e9251faa5d4d40a6d7"},
{file = "zstd-1.5.7.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b13285c99cc710f60dd270785ec75233018870a1831f5655d862745470a0ca29"},
{file = "zstd-1.5.7.2-cp311-cp311-win32.whl", hash = "sha256:cdb5ec80da299f63f8aeccec0bff3247e96252d4c8442876363ff1b438d8049b"},
{file = "zstd-1.5.7.2-cp311-cp311-win_amd64.whl", hash = "sha256:4f6861c8edceb25fda37cdaf422fc5f15dcc88ced37c6a5b3c9011eda51aa218"},
{file = "zstd-1.5.7.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d2ebe3e60dbace52525fa7aa604479e231dc3e4fcc76d0b4c54d8abce5e58734"},
{file = "zstd-1.5.7.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ef201b6f7d3a6751d85cc52f9e6198d4d870e83d490172016b64a6dd654a9583"},
{file = "zstd-1.5.7.2-cp312-cp312-manylinux_2_14_x86_64.whl", hash = "sha256:ac7bdfedda51b1fcdcf0ab69267d01256fc97ddf666ce894fde0fae9f3630eac"},
{file = "zstd-1.5.7.2-cp312-cp312-manylinux_2_4_i686.whl", hash = "sha256:b835405cc4080b378e45029f2fe500e408d1eaedfba7dd7402aba27af16955f9"},
{file = "zstd-1.5.7.2-cp312-cp312-win32.whl", hash = "sha256:e4cf97bb97ed6dbb62d139d68fd42fa1af51fd26fd178c501f7b62040e897c50"},
{file = "zstd-1.5.7.2-cp312-cp312-win_amd64.whl", hash = "sha256:55e2edc4560a5cf8ee9908595e90a15b1f47536ea9aad4b2889f0e6165890a38"},
{file = "zstd-1.5.7.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:6e684e27064b6550aa2e7dc85d171ea1b62cb5930a2c99b3df9b30bf620b5c06"},
{file = "zstd-1.5.7.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:fd6262788a98807d6b2befd065d127db177c1cd76bb8e536e0dded419eb7c7fb"},
{file = "zstd-1.5.7.2-cp313-cp313-manylinux_2_14_x86_64.whl", hash = "sha256:53948be45f286a1b25c07a6aa2aca5c902208eb3df9fe36cf891efa0394c8b71"},
{file = "zstd-1.5.7.2-cp313-cp313-win32.whl", hash = "sha256:edf816c218e5978033b7bb47dcb453dfb71038cb8a9bf4877f3f823e74d58174"},
{file = "zstd-1.5.7.2-cp313-cp313-win_amd64.whl", hash = "sha256:eea9bddf06f3f5e1e450fd647665c86df048a45e8b956d53522387c1dff41b7a"},
{file = "zstd-1.5.7.2-cp313-cp313t-manylinux_2_14_x86_64.whl", hash = "sha256:1d71f9f92b3abe18b06b5f0aefa5b9c42112beef3bff27e36028d147cb4426a6"},
{file = "zstd-1.5.7.2-cp314-cp314-manylinux_2_14_x86_64.whl", hash = "sha256:a6105b8fa21dbc59e05b6113e8e5d5aaf56c5d2886aa5778d61030af3256bbb7"},
{file = "zstd-1.5.7.2-cp314-cp314t-manylinux_2_14_x86_64.whl", hash = "sha256:d0b0ca097efb5f67157c61a744c926848dcccf6e913df2f814e719aa78197a4b"},
{file = "zstd-1.5.7.2-cp34-cp34m-manylinux_2_4_i686.whl", hash = "sha256:a371274668182ae06be2e321089b207fa0a75a58ae2fd4dfb7eafded9e041b2f"},
{file = "zstd-1.5.7.2-cp34-cp34m-manylinux_2_4_x86_64.whl", hash = "sha256:74c3f006c9a3a191ed454183f0fb78172444f5cb431be04d85044a27f1b58c7b"},
{file = "zstd-1.5.7.2-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:f19a3e658d92b6b52020c4c6d4c159480bcd3b47658773ea0e8d343cee849f33"},
{file = "zstd-1.5.7.2-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:d9d1bcb6441841c599883139c1b0e47bddb262cce04b37dc2c817da5802c1158"},
{file = "zstd-1.5.7.2-cp35-cp35m-manylinux2014_aarch64.whl", hash = "sha256:bb1cb423fc40468cc9b7ab51a5b33c618eefd2c910a5bffed6ed76fe1cbb20b0"},
{file = "zstd-1.5.7.2-cp35-cp35m-manylinux_2_14_x86_64.whl", hash = "sha256:e2476ba12597e58c5fc7a3ae547ee1bef9dd6b9d5ea80cf8d4034930c5a336e0"},
{file = "zstd-1.5.7.2-cp35-cp35m-manylinux_2_4_i686.whl", hash = "sha256:2bf6447373782a2a9df3015121715f6d0b80a49a884c2d7d4518c9571e9fca16"},
{file = "zstd-1.5.7.2-cp35-cp35m-win32.whl", hash = "sha256:a59a136a9eaa1849d715c004e30344177e85ad6e7bc4a5d0b6ad2495c5402675"},
{file = "zstd-1.5.7.2-cp35-cp35m-win_amd64.whl", hash = "sha256:114115af8c68772a3205414597f626b604c7879f6662a2a79c88312e0f50361f"},
{file = "zstd-1.5.7.2-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:f576ec00e99db124309dac1e1f34bc320eb69624189f5fdaf9ebe1dc81581a84"},
{file = "zstd-1.5.7.2-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:f97d8593da0e23a47f148a1cb33300dccd513fb0df9f7911c274e228a8c1a300"},
{file = "zstd-1.5.7.2-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:a130243e875de5aeda6099d12b11bc2fcf548dce618cf6b17f731336ba5338e4"},
{file = "zstd-1.5.7.2-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:73cec37649fda383348dc8b3b5fba535f1dbb1bbaeb60fd36f4c145820208619"},
{file = "zstd-1.5.7.2-cp36-cp36m-manylinux_2_14_x86_64.whl", hash = "sha256:883e7b77a3124011b8badd0c7c9402af3884700a3431d07877972e157d85afb8"},
{file = "zstd-1.5.7.2-cp36-cp36m-manylinux_2_4_i686.whl", hash = "sha256:b5af6aa041b5515934afef2ef4af08566850875c3c890109088eedbe190eeefb"},
{file = "zstd-1.5.7.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:53abf577aec7b30afa3c024143f4866676397c846b44f1b30d8097b5e4f5c7d7"},
{file = "zstd-1.5.7.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:660945ba16c16957c94dafc40aff1db02a57af0489aa3a896866239d47bb44b0"},
{file = "zstd-1.5.7.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:3e220d2d7005822bb72a52e76410ca4634f941d8062c08e8e3285733c63b1db7"},
{file = "zstd-1.5.7.2-cp37-cp37m-manylinux_2_4_i686.whl", hash = "sha256:7e998f86a9d1e576c0158bf0b0a6a5c4685679d74ba0053a2e87f684f9bdc8eb"},
{file = "zstd-1.5.7.2-cp37-cp37m-manylinux_2_4_x86_64.whl", hash = "sha256:70d0c4324549073e05aa72e9eb6a593f89cba59da804b946d325d68467b93ad5"},
{file = "zstd-1.5.7.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_24_i686.whl", hash = "sha256:b9518caabf59405eddd667bbb161d9ae7f13dbf96967fd998d095589c8d41c86"},
{file = "zstd-1.5.7.2-cp37-cp37m-musllinux_1_2_aarch64.whl", hash = "sha256:30d339d8e5c4b14c2015b50371fcdb8a93b451ca6d3ef813269ccbb8b3b3ef7d"},
{file = "zstd-1.5.7.2-cp37-cp37m-musllinux_1_2_i686.whl", hash = "sha256:6f5539a10b838ee576084870eed65b63c13845e30a5b552cfe40f7e6b621e61a"},
{file = "zstd-1.5.7.2-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:5540ce1c99fa0b59dad2eff771deb33872754000da875be50ac8c2beab42b433"},
{file = "zstd-1.5.7.2-cp37-cp37m-win32.whl", hash = "sha256:56c4b8cd0a88fd721213661c28b87b64fbd14b6019df39b21b0117a68162b0f2"},
{file = "zstd-1.5.7.2-cp37-cp37m-win_amd64.whl", hash = "sha256:594f256fa72852ade60e3acb909f983d5cf6839b9fc79728dd4b48b31112058f"},
{file = "zstd-1.5.7.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:9dc05618eb0abceb296b77e5f608669c12abc69cbf447d08151bcb14d290ab07"},
{file = "zstd-1.5.7.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:70231ba799d681b6fc17456c3e39895c493b5dff400aa7842166322a952b7f2a"},
{file = "zstd-1.5.7.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:5a73f0f20f71d4eef970a3fed7baac64d9a2a00b238acc4eca2bd7172bd7effb"},
{file = "zstd-1.5.7.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:0a470f8938f69f632b8f88b96578a5e8825c18ddbbea7de63493f74874f963ef"},
{file = "zstd-1.5.7.2-cp38-cp38-manylinux_2_4_i686.whl", hash = "sha256:d104f1cb2a7c142007c29a2a62dfe633155c648317a465674e583c295e5f792d"},
{file = "zstd-1.5.7.2-cp38-cp38-manylinux_2_4_x86_64.whl", hash = "sha256:70f29e0504fc511d4b9f921e69637fca79c050e618ba23732a3f75c044814d89"},
{file = "zstd-1.5.7.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_24_i686.whl", hash = "sha256:a62c2f6f7b8fc69767392084828740bd6faf35ff54d4ccb2e90e199327c64140"},
{file = "zstd-1.5.7.2-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:f2dda0c76f87723fb7f75d7ad3bbd90f7fb47b75051978d22535099325111b41"},
{file = "zstd-1.5.7.2-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:f9cf09c2aa6f67750fe9f33fdd122f021b1a23bf7326064a8e21f7af7e77faee"},
{file = "zstd-1.5.7.2-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:910bd9eac2488439f597504756b03c74aa63ed71b21e5d0aa2c7e249b3f1c13f"},
{file = "zstd-1.5.7.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9838ec7eb9f1beb2f611b9bcac7a169cb3de708ccf779aead29787e4482fe232"},
{file = "zstd-1.5.7.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:83a36bb1fd574422a77b36ccf3315ab687aef9a802b0c3312ca7006b74eeb109"},
{file = "zstd-1.5.7.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:6f8189bc58415758bbbd419695012194f5e5e22c34553712d9a3eb009c09808d"},
{file = "zstd-1.5.7.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:632e3c1b7e1ebb0580f6d92b781a8f7901d367cf72725d5642e6d3a32e404e45"},
{file = "zstd-1.5.7.2-cp39-cp39-manylinux_2_4_i686.whl", hash = "sha256:df8083c40fdbfe970324f743f0b5ecc244c37736e5f3ad2670de61dde5e0b024"},
{file = "zstd-1.5.7.2-cp39-cp39-manylinux_2_4_x86_64.whl", hash = "sha256:300db1ede4d10f8b9b3b99ca52b22f0e2303dc4f1cf6994d1f8345ce22dd5a7e"},
{file = "zstd-1.5.7.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_24_i686.whl", hash = "sha256:97b908ccb385047b0c020ce3dc55e6f51078c9790722fdb3620c076be4a69ecf"},
{file = "zstd-1.5.7.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:c59218bd36a7431a40591504f299de836ea0d63bc68ea76d58c4cf5262f0fa3c"},
{file = "zstd-1.5.7.2-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:4d5a85344193ec967d05da8e2c10aed400e2d83e16041d2fdfb713cfc8caceeb"},
{file = "zstd-1.5.7.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:ebf6c1d7f0ceb0af5a383d2a1edc8ab9ace655e62a41c8a4ed5a031ee2ef8006"},
{file = "zstd-1.5.7.2-cp39-cp39-win32.whl", hash = "sha256:44a5142123d59a0dbbd9ba9720c23521be57edbc24202223a5e17405c3bdd4a6"},
{file = "zstd-1.5.7.2-cp39-cp39-win_amd64.whl", hash = "sha256:8dc542a9818712a9fb37563fa88cdbbbb2b5f8733111d412b718fa602b83ba45"},
{file = "zstd-1.5.7.2-pp27-pypy_73-manylinux1_x86_64.whl", hash = "sha256:24371a7b0475eef7d933c72067d363c5dc17282d2aa5d4f5837774378718509e"},
{file = "zstd-1.5.7.2-pp27-pypy_73-manylinux2010_x86_64.whl", hash = "sha256:c21d44981b068551f13097be3809fadb7f81617d0c21b2c28a7d04653dde958f"},
{file = "zstd-1.5.7.2-pp27-pypy_73-manylinux_2_14_x86_64.whl", hash = "sha256:b011bf4cfad78cdf9116d6731234ff181deb9560645ffdcc8d54861ae5d1edfc"},
{file = "zstd-1.5.7.2-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:426e5c6b7b3e2401b734bfd08050b071e17c15df5e3b31e63651d1fd9ba4c751"},
{file = "zstd-1.5.7.2-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:53375b23f2f39359ade944169bbd88f8895eed91290ee608ccbc28810ac360ba"},
{file = "zstd-1.5.7.2-pp310-pypy310_pp73-manylinux_2_14_x86_64.whl", hash = "sha256:1b301b2f9dbb0e848093127fb10cbe6334a697dc3aea6740f0bb726450ee9a34"},
{file = "zstd-1.5.7.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:5414c9ae27069ab3ec8420fe8d005cb1b227806cbc874a7b4c73a96b4697a633"},
{file = "zstd-1.5.7.2-pp311-pypy311_pp73-manylinux_2_14_x86_64.whl", hash = "sha256:5fb2ff5718fe89181223c23ce7308bd0b4a427239379e2566294da805d8df68a"},
{file = "zstd-1.5.7.2-pp36-pypy36_pp73-manylinux1_x86_64.whl", hash = "sha256:9714d5642867fceb22e4ab74aebf81a2e62dc9206184d603cb39277b752d5885"},
{file = "zstd-1.5.7.2-pp36-pypy36_pp73-manylinux2010_x86_64.whl", hash = "sha256:6584fd081a6e7d92dffa8e7373d1fced6b3cbf473154b82c17a99438c5e1de51"},
{file = "zstd-1.5.7.2-pp36-pypy36_pp73-manylinux_2_14_x86_64.whl", hash = "sha256:52f27a198e2a72632bae12ec63ebaa31b10e3d5f3dd3df2e01376979b168e2e6"},
{file = "zstd-1.5.7.2-pp36-pypy36_pp73-win32.whl", hash = "sha256:3b14793d2a2cb3a7ddd1cf083321b662dd20bc11143abc719456e9bfd22a32aa"},
{file = "zstd-1.5.7.2-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:faf3fd38ba26167c5a085c04b8c931a216f1baf072709db7a38e61dea52e316e"},
{file = "zstd-1.5.7.2-pp37-pypy37_pp73-manylinux_2_14_x86_64.whl", hash = "sha256:d17ac6d2584168247796174e599d4adbee00153246287e68881efaf8d48a6970"},
{file = "zstd-1.5.7.2-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:9a24d492c63555b55e6bc73a9e82a38bf7c3e8f7cde600f079210ed19cb061f2"},
{file = "zstd-1.5.7.2-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:c6abf4ab9a9d1feb14bc3cbcc32d723d340ce43b79b1812805916f3ac069b073"},
{file = "zstd-1.5.7.2-pp37-pypy37_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_24_i686.whl", hash = "sha256:d7131bb4e55d075cb7847555a1e17fca5b816a550c9b9ac260c01799b6f8e8d9"},
{file = "zstd-1.5.7.2-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:a03608499794148f39c932c508d4eb3622e79ca2411b1d0438a2ee8cafdc0111"},
{file = "zstd-1.5.7.2-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:86e64c71b4d00bf28be50e4941586e7874bdfa74858274d9f7571dd5dda92086"},
{file = "zstd-1.5.7.2-pp38-pypy38_pp73-macosx_11_0_arm64.whl", hash = "sha256:0f79492bf86aef6e594b11e29c5589ddd13253db3ada0c7a14fb176b132fb65e"},
{file = "zstd-1.5.7.2-pp38-pypy38_pp73-manylinux_2_14_x86_64.whl", hash = "sha256:8c3f4bb8508bc54c00532931da4a5261f08493363da14a5526c986765973e35d"},
{file = "zstd-1.5.7.2-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:787bcf55cefc08d27aca34c6dcaae1a24940963d1a73d4cec894ee458c541ac4"},
{file = "zstd-1.5.7.2-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:0f97f872cb78a4fd60b6c1024a65a4c52a971e9d991f33c7acd833ee73050f85"},
{file = "zstd-1.5.7.2-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_24_i686.whl", hash = "sha256:5e530b75452fdcff4ea67268d9e7cb37a38e7abbac84fa845205f0b36da81aaf"},
{file = "zstd-1.5.7.2-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:7c1cc65fc2789dd97a98202df840537de186ed04fd1804a17fcb15d1232442c4"},
{file = "zstd-1.5.7.2-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:05604a693fa53b60ca083992324b08dafd15a4ac37ac4cffe4b43b9eb93d4440"},
{file = "zstd-1.5.7.2-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:baf4e8b46d8934d4e85373f303eb048c63897fc4191d8ab301a1bbdf30b7a3cc"},
{file = "zstd-1.5.7.2-pp39-pypy39_pp73-manylinux_2_14_x86_64.whl", hash = "sha256:8cc35cc25e2d4a0f68020f05cba96912a2881ebaca890d990abe37aa3aa27045"},
{file = "zstd-1.5.7.2-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:ceae57e369e1b821b8f2b4c59bc08acd27d8e4bf9687bfa5211bc4cdb080fe7b"},
{file = "zstd-1.5.7.2-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:5189fb44c44ab9b6c45f734bd7093a67686193110dc90dcfaf0e3a31b2385f38"},
{file = "zstd-1.5.7.2-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_24_i686.whl", hash = "sha256:f51a965871b25911e06d421212f9be7f7bcd3cedc43ea441a8a73fad9952baa0"},
{file = "zstd-1.5.7.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:624022851c51dd6d6b31dbfd793347c4bd6339095e8383e2f74faf4f990b04c6"},
{file = "zstd-1.5.7.2.tar.gz", hash = "sha256:6d8684c69009be49e1b18ec251a5eb0d7e24f93624990a8a124a1da66a92fc8a"},
]
[metadata]
lock-version = "2.1"
python-versions = ">=3.11,<3.13"
content-hash = "dd974908bc16c3730c76f18506e8a5cdd1b726965ab8fa05336b22ab1b5ab7be"
content-hash = "77ef098291cb8631565a1ab5027ce33e7fcb5a04883dc7160bf373eac9e1fb49"
+1 -1
View File
@@ -24,7 +24,7 @@ dependencies = [
"drf-spectacular-jsonapi==0.5.1",
"gunicorn==23.0.0",
"lxml==5.3.2",
"prowler @ git+https://github.com/prowler-cloud/prowler.git@v5.15",
"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)",
@@ -1,30 +0,0 @@
# Generated by Django 5.1.14 on 2025-12-10
from django.db import migrations
from tasks.tasks import backfill_daily_severity_summaries_task
from api.db_router import MainRouter
from api.rls import Tenant
def trigger_backfill_task(apps, schema_editor):
"""
Trigger the backfill task for all tenants.
This dispatches backfill_daily_severity_summaries_task for each tenant
in the system to populate DailySeveritySummary records from historical scans.
"""
tenant_ids = Tenant.objects.using(MainRouter.admin_db).values_list("id", flat=True)
for tenant_id in tenant_ids:
backfill_daily_severity_summaries_task.delay(tenant_id=str(tenant_id), days=90)
class Migration(migrations.Migration):
dependencies = [
("api", "0061_daily_severity_summary"),
]
operations = [
migrations.RunPython(trigger_backfill_task, migrations.RunPython.noop),
]
+4 -2
View File
@@ -1,8 +1,6 @@
from collections import defaultdict
from datetime import timedelta
from django.db.models import Sum
from django.utils import timezone
from api.db_router import READ_REPLICA_ALIAS
from api.db_utils import rls_transaction
@@ -188,6 +186,10 @@ def backfill_daily_severity_summaries(tenant_id: str, days: int = None):
Backfill DailySeveritySummary from completed scans.
Groups by provider+date, keeps latest scan per day.
"""
from datetime import timedelta
from django.utils import timezone
created_count = 0
updated_count = 0
-76
View File
@@ -63,82 +63,6 @@ Other Commands for Running Tests
Refer to the [pytest documentation](https://docs.pytest.org/en/7.1.x/getting-started.html) for more details.
</Note>
## AWS Service Dependency Table (CI Optimization)
To optimize CI pipeline execution time, the GitHub Actions workflow for AWS tests uses a **service dependency table** that determines which tests to run based on changed files. This ensures that when a service is modified, all dependent services are also tested.
### How It Works
The dependency table is defined in `.github/workflows/sdk-tests.yml` within the "Resolve AWS services under test" step. When files in a specific AWS service are changed:
1. Tests for the changed service are run
2. Tests for all services that **depend on** the changed service are also run
For example, if you modify the `ec2` service, tests will also run for `dlm`, `dms`, `elbv2`, `emr`, `inspector2`, `rds`, `redshift`, `route53`, `shield`, `ssm`, and `workspaces` because these services use the EC2 client.
### Current Dependency Table
The table maps a service (key) to the list of services that depend on it (values):
| Service | Dependent Services |
|---------|-------------------|
| `acm` | `elb` |
| `autoscaling` | `dynamodb` |
| `awslambda` | `ec2`, `inspector2` |
| `backup` | `dynamodb`, `ec2`, `rds` |
| `cloudfront` | `shield` |
| `cloudtrail` | `awslambda`, `cloudwatch` |
| `cloudwatch` | `bedrock` |
| `ec2` | `dlm`, `dms`, `elbv2`, `emr`, `inspector2`, `rds`, `redshift`, `route53`, `shield`, `ssm` |
| `ecr` | `inspector2` |
| `elb` | `shield` |
| `elbv2` | `shield` |
| `globalaccelerator` | `shield` |
| `iam` | `bedrock`, `cloudtrail`, `cloudwatch`, `codebuild` |
| `kafka` | `firehose` |
| `kinesis` | `firehose` |
| `kms` | `kafka` |
| `organizations` | `iam`, `servicecatalog` |
| `route53` | `shield` |
| `s3` | `bedrock`, `cloudfront`, `cloudtrail`, `macie` |
| `ssm` | `ec2` |
| `vpc` | `awslambda`, `ec2`, `efs`, `elasticache`, `neptune`, `networkfirewall`, `rds`, `redshift`, `workspaces` |
| `waf` | `elbv2` |
| `wafv2` | `cognito`, `elbv2` |
### When to Update the Table
You must update the dependency table when:
1. **A new check or service uses another service's client**: If your check imports a client from another service (e.g., `from prowler.providers.aws.services.ec2.ec2_client import ec2_client` in a non-ec2 check), add your service to the dependent services list of that client's service.
2. **A service relationship changes**: If you remove or add a service client dependency in an existing check, update the table accordingly.
### How to Update the Table
1. Open `.github/workflows/sdk-tests.yml`
2. Find the `dependents` dictionary in the "Resolve AWS services under test" step
3. Add or modify entries as needed
4. **Update this documentation page** (`docs/developer-guide/unit-testing.mdx`) to reflect the changes in the [Current Dependency Table](#current-dependency-table) section above
```python
dependents = {
# ... existing entries ...
"service_being_used": ["service_that_uses_it"],
}
```
**Example**: If you create a new check in the `newservice` service that imports `ec2_client`, add `newservice` to the `ec2` entry:
```python
"ec2": ["dlm", "dms", "elbv2", "emr", "inspector2", "newservice", "rds", "redshift", "route53", "shield", "ssm"],
```
<Warning>
Failing to update this table when adding cross-service dependencies may result in CI tests passing even when related functionality is broken, as the dependent service tests won't be triggered.
</Warning>
## AWS Testing Approaches
For AWS provider, different testing approaches apply based on API coverage based on several criteria.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 743 KiB

After

Width:  |  Height:  |  Size: 420 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 690 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 872 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 552 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 MiB

@@ -58,7 +58,7 @@ Before you begin, ensure you have:
### Authentication
Prowler supports multiple authentication methods for OCI. For detailed authentication setup, see the [OCI Authentication Guide](./authentication).
Prowler supports multiple authentication methods for OCI. For detailed authentication setup, see the [OCI Authentication Guide](./authentication.mdx).
**Note:** OCI Session Authentication and Config File Authentication both use the same `~/.oci/config` file. The difference is how the config file is generated - automatically via browser (session auth) or manually with API keys.
@@ -107,7 +107,7 @@ The easiest and most secure method is using OCI session authentication, which au
#### Alternative: Manual API Key Setup
If you prefer to manually generate API keys instead of using browser-based session authentication, see the detailed instructions in the [Authentication Guide](./authentication#config-file-authentication-manual-api-key-setup).
If you prefer to manually generate API keys instead of using browser-based session authentication, see the detailed instructions in the [Authentication Guide](./authentication.mdx#config-file-authentication-manual-api-key-setup).
**Note:** Both methods use the same `~/.oci/config` file - the difference is that manual setup uses static API keys while session authentication uses temporary session tokens.
+1 -5
View File
@@ -2,15 +2,11 @@
All notable changes to the **Prowler MCP Server** are documented in this file.
## [0.2.0] (Prowler v5.15.0)
## [0.2.0] (Prowler UNRELEASED)
### Added
- Remove all Prowler App MCP tools; and add new MCP Server tools for Prowler Findings and Compliance [(#9300)](https://github.com/prowler-cloud/prowler/pull/9300)
- Add new MCP Server tools for Prowler Providers Management [(#9350)](https://github.com/prowler-cloud/prowler/pull/9350)
- Add new MCP Server tools for Prowler Resources Management [(#9380)](https://github.com/prowler-cloud/prowler/pull/9380)
- Add new MCP Server tools for Prowler Scans Management [(#9509)](https://github.com/prowler-cloud/prowler/pull/9509)
- Add new MCP Server tools for Prowler Muting Management [(#9510)](https://github.com/prowler-cloud/prowler/pull/9510)
---
@@ -1,6 +1,7 @@
"""Pydantic models for Prowler App MCP Server."""
from prowler_mcp_server.prowler_app.models.base import MinimalSerializerMixin
from prowler_mcp_server.prowler_app.models.findings import (
CheckMetadata,
CheckRemediation,
@@ -9,12 +10,6 @@ from prowler_mcp_server.prowler_app.models.findings import (
FindingsOverview,
SimplifiedFinding,
)
from prowler_mcp_server.prowler_app.models.muting import (
DetailedMuteRule,
MutelistResponse,
MuteRulesListResponse,
SimplifiedMuteRule,
)
__all__ = [
# Base models
@@ -26,9 +21,4 @@ __all__ = [
"FindingsListResponse",
"FindingsOverview",
"SimplifiedFinding",
# Muting models
"DetailedMuteRule",
"MutelistResponse",
"MuteRulesListResponse",
"SimplifiedMuteRule",
]
@@ -27,19 +27,18 @@ class MinimalSerializerMixin(BaseModel):
Dictionary with non-empty values only
"""
data = handler(self)
return {k: v for k, v in data.items() if not self._should_exclude(k, v)}
return {k: v for k, v in data.items() if not self._should_exclude(v)}
def _should_exclude(self, key: str, value: Any) -> bool:
"""Determine if a key-value pair should be excluded from serialization.
def _should_exclude(self, value: Any) -> bool:
"""Determine if a value should be excluded from serialization.
Override this method in subclasses for custom exclusion logic.
Args:
key: Field name
value: Field value
Returns:
True if the field should be excluded, False otherwise
True if the value should be excluded, False otherwise
"""
# None values
if value is None:
@@ -19,17 +19,12 @@ class CheckRemediation(MinimalSerializerMixin, BaseModel):
default=None,
description="Terraform code snippet with best practices for remediation",
)
nativeiac: str | None = Field(
default=None,
description="Native Infrastructure as Code code snippet with best practices for remediation",
recommendation_text: str | None = Field(
default=None, description="Text description with best practices"
)
other: str | None = Field(
recommendation_url: str | None = Field(
default=None,
description="Other remediation code snippet with best practices for remediation, usually used for web interfaces or other tools",
)
recommendation: str | None = Field(
default=None,
description="Text description with general best recommended practices to avoid the issue",
description="URL to external remediation documentation",
)
@@ -38,6 +33,9 @@ class CheckMetadata(MinimalSerializerMixin, BaseModel):
model_config = ConfigDict(frozen=True)
check_id: str = Field(
description="Unique provider identifier for the security check (e.g., 's3_bucket_public_access')",
)
title: str = Field(
description="Human-readable title of the security check",
)
@@ -61,9 +59,9 @@ class CheckMetadata(MinimalSerializerMixin, BaseModel):
default=None,
description="Remediation guidance including CLI commands and recommendations",
)
additional_urls: list[str] = Field(
default_factory=list,
description="List of additional URLs related to the check",
related_url: str | None = Field(
default=None,
description="URL to additional documentation or references",
)
categories: list[str] = Field(
default_factory=list,
@@ -81,23 +79,23 @@ class CheckMetadata(MinimalSerializerMixin, BaseModel):
recommendation = remediation_data.get("recommendation", {})
remediation = CheckRemediation(
cli=code["cli"],
terraform=code["terraform"],
nativeiac=code["nativeiac"],
other=code["other"],
recommendation=recommendation["text"],
cli=code.get("cli"),
terraform=code.get("terraform"),
recommendation_text=recommendation.get("text"),
recommendation_url=recommendation.get("url"),
)
return cls(
check_id=data["checkid"],
title=data["checktitle"],
description=data["description"],
provider=data["provider"],
risk=data["risk"],
risk=data.get("risk"),
service=data["servicename"],
resource_type=data["resourcetype"],
remediation=remediation,
additional_urls=data["additionalurls"],
categories=data["categories"],
related_url=data.get("relatedurl"),
categories=data.get("categories", []),
)
@@ -118,36 +116,35 @@ class SimplifiedFinding(MinimalSerializerMixin, BaseModel):
severity: Literal["critical", "high", "medium", "low", "informational"] = Field(
description="Severity level of the finding",
)
check_id: str = Field(
description="ID of the security check that generated this finding",
check_metadata: CheckMetadata = Field(
description="Metadata about the security check that generated this finding",
)
status_extended: str = Field(
description="Extended status information providing additional context",
)
delta: Literal["new", "changed"] | None = Field(
default=None,
delta: Literal["new", "changed"] = Field(
description="Change status: 'new' (not seen before), 'changed' (modified since last scan), or None (unchanged)",
)
muted: bool | None = Field(
default=None,
muted: bool = Field(
description="Whether this finding has been muted/suppressed by the user",
)
muted_reason: str | None = Field(
muted_reason: str = Field(
default=None,
description="Reason provided when muting this finding",
description="Reason provided when muting this finding (3-500 chars if muted)",
)
@classmethod
def from_api_response(cls, data: dict) -> "SimplifiedFinding":
"""Transform JSON:API finding response to simplified format."""
attributes = data["attributes"]
check_metadata = attributes["check_metadata"]
return cls(
id=data["id"],
uid=attributes["uid"],
status=attributes["status"],
severity=attributes["severity"],
check_id=attributes["check_metadata"]["checkid"],
check_metadata=CheckMetadata.from_api_response(check_metadata),
status_extended=attributes["status_extended"],
delta=attributes["delta"],
muted=attributes["muted"],
@@ -182,9 +179,6 @@ class DetailedFinding(SimplifiedFinding):
default_factory=list,
description="List of UUIDs for cloud resources associated with this finding",
)
check_metadata: CheckMetadata = Field(
description="Metadata about the security check that generated this finding",
)
@classmethod
def from_api_response(cls, data: dict) -> "DetailedFinding":
@@ -210,7 +204,6 @@ class DetailedFinding(SimplifiedFinding):
uid=attributes["uid"],
status=attributes["status"],
severity=attributes["severity"],
check_id=check_metadata["checkid"],
check_metadata=CheckMetadata.from_api_response(check_metadata),
status_extended=attributes.get("status_extended"),
delta=attributes.get("delta"),
@@ -1,196 +0,0 @@
"""Pydantic models for simplified muting responses."""
from typing import Any
from prowler_mcp_server.prowler_app.models.base import MinimalSerializerMixin
from pydantic import BaseModel, ConfigDict, Field
class MutelistResponse(MinimalSerializerMixin, BaseModel):
"""Simplified mutelist response with Prowler configuration.
Represents a mutelist configuration that defines which findings
should be automatically muted based on account patterns, check IDs, regions,
resources, tags, and exceptions.
"""
model_config = ConfigDict(frozen=True)
id: str = Field(
description="Unique UUIDv4 identifier for this mutelist in Prowler database"
)
configuration: dict[str, Any] = Field(
description="Mutelist configuration following Prowler format with nested structure: Mutelist → Accounts → Checks → Regions/Resources/Tags/Exceptions"
)
inserted_at: str | None = Field(
default=None,
description="ISO 8601 timestamp when this mutelist was created",
)
updated_at: str | None = Field(
default=None,
description="ISO 8601 timestamp when this mutelist was last modified",
)
@classmethod
def from_api_response(cls, data: dict[str, Any]) -> "MutelistResponse":
"""Transform JSON:API processor response to simplified format.
The configuration structure follows the Prowler mutelist format:
{
"Mutelist": {
"Accounts": {
"<account-pattern>": {
"Checks": {
"<check-id>": {
"Regions": [...],
"Resources": [...],
"Tags": [...],
"Exceptions": {...}
}
}
}
}
}
}
"""
attributes = data.get("attributes", {})
return cls(
id=data["id"],
configuration=attributes.get("configuration", {}),
inserted_at=attributes.get("inserted_at"),
updated_at=attributes.get("updated_at"),
)
class SimplifiedMuteRule(MinimalSerializerMixin, BaseModel):
"""Simplified mute rule for list/search operations.
Provides lightweight mute rule information without the full list of finding UIDs.
Use this for listing and searching operations where you need basic rule information
but don't need the complete list of affected findings.
"""
model_config = ConfigDict(frozen=True)
id: str = Field(
description="Unique UUIDv4 identifier for this mute rule in Prowler database"
)
name: str = Field(description="Human-readable name for this mute rule")
reason: str = Field(description="Documented reason for muting these findings")
enabled: bool = Field(
description="Whether this mute rule is currently active and applying muting to findings"
)
finding_count: int = Field(
description="Number of findings currently muted by this rule", ge=0
)
inserted_at: str | None = Field(
default=None,
description="ISO 8601 timestamp when this mute rule was created",
)
updated_at: str | None = Field(
default=None,
description="ISO 8601 timestamp when this mute rule was last modified",
)
@classmethod
def from_api_response(cls, data: dict[str, Any]) -> "SimplifiedMuteRule":
"""Transform JSON:API mute rule response to simplified format."""
attributes = data.get("attributes", {})
# Calculate finding count from finding_uids list length
finding_uids = attributes.get("finding_uids", [])
return cls(
id=data["id"],
name=attributes["name"],
reason=attributes["reason"],
enabled=attributes["enabled"],
finding_count=len(finding_uids),
inserted_at=attributes.get("inserted_at"),
updated_at=attributes.get("updated_at"),
)
class DetailedMuteRule(SimplifiedMuteRule):
"""Detailed mute rule with complete information including finding UIDs.
Extends SimplifiedMuteRule with the full list of finding UIDs being muted and
creator information (user/service account that created the rule).
Use this when you need complete context about a specific mute rule, including
all affected findings and audit trail information.
"""
finding_uids: list[str] = Field(
description="List of finding UIDs that are muted by this rule"
)
user_creator_id: str | None = Field(
default=None,
description="UUIDv4 identifier of the Prowler user from the tenant that created this rule",
)
@classmethod
def from_api_response(cls, data: dict[str, Any]) -> "DetailedMuteRule":
"""Transform JSON:API mute rule response to detailed format."""
attributes = data.get("attributes", {})
relationships = data.get("relationships", {})
# Extract creator information
user_creator_id = None
creator_data = relationships.get("created_by", {}).get("data")
if creator_data:
user_creator_id = creator_data.get("id")
finding_uids = attributes.get("finding_uids", [])
return cls(
id=data["id"],
name=attributes["name"],
reason=attributes["reason"],
enabled=attributes["enabled"],
finding_count=len(finding_uids),
finding_uids=finding_uids,
inserted_at=attributes.get("inserted_at"),
updated_at=attributes.get("updated_at"),
user_creator_id=user_creator_id,
)
class MuteRulesListResponse(BaseModel):
"""Simplified response for mute rules list queries with pagination.
Contains a list of simplified mute rules and pagination metadata.
Use this for paginated list/search operations to get multiple rules efficiently.
"""
model_config = ConfigDict(frozen=True)
mute_rules: list[SimplifiedMuteRule] = Field(
description="List of simplified mute rules matching the query filters"
)
total_num_mute_rules: int = Field(
description="Total number of mute rules matching the query across all pages",
ge=0,
)
total_num_pages: int = Field(
description="Total number of pages available for the query results", ge=0
)
current_page: int = Field(
description="Current page number in the paginated results (1-indexed)", ge=1
)
@classmethod
def from_api_response(cls, response: dict[str, Any]) -> "MuteRulesListResponse":
"""Transform JSON:API response to simplified format."""
data = response.get("data", [])
meta = response.get("meta", {})
pagination = meta.get("pagination", {})
mute_rules = [SimplifiedMuteRule.from_api_response(item) for item in data]
return cls(
mute_rules=mute_rules,
total_num_mute_rules=pagination.get("count", 0),
total_num_pages=pagination.get("pages", 1),
current_page=pagination.get("page", 1),
)
@@ -1,134 +0,0 @@
"""Pydantic models for simplified provider responses."""
from typing import Any, Literal
from prowler_mcp_server.prowler_app.models.base import MinimalSerializerMixin
from pydantic import BaseModel
class SimplifiedProvider(MinimalSerializerMixin, BaseModel):
"""Simplified provider for list/search operations."""
id: str
uid: str
alias: str | None = None
provider: str
connected: bool | None = None
secret_type: Literal["role", "service_account", "static"] | None = None
def _should_exclude(self, key: str, value: Any) -> bool:
"""Override to always include connected and secret_type fields even when None."""
# Always include these fields regardless of value (None has semantic meaning)
if key == "connected" or key == "secret_type":
return False
# Use parent class logic for other fields
return super()._should_exclude(key, value)
@classmethod
def from_api_response(cls, data: dict[str, Any]) -> "SimplifiedProvider":
"""Transform JSON:API provider response to simplified format."""
attributes = data["attributes"]
connection_data = attributes.get("connection", {})
return cls(
id=data["id"],
uid=attributes["uid"],
alias=attributes.get("alias"),
provider=attributes["provider"],
connected=connection_data.get("connected"),
secret_type=None, # Will be populated separately via secret endpoint
)
class DetailedProvider(SimplifiedProvider):
"""Detailed provider with complete information for deep analysis.
Extends SimplifiedProvider with temporal metadata and relationships.
Use this when you need complete context about a specific provider.
"""
inserted_at: str | None = None
updated_at: str | None = None
last_checked_at: str | None = None
provider_group_ids: list[str] | None = None
@classmethod
def from_api_response(cls, data: dict[str, Any]) -> "DetailedProvider":
"""Transform JSON:API provider response to detailed format."""
attributes = data["attributes"]
connection_data = attributes.get("connection", {})
relationships = data.get("relationships", {})
# Extract provider groups relationship
provider_group_ids = None
groups_data = relationships.get("provider_groups", {}).get("data", [])
if groups_data:
provider_group_ids = [group["id"] for group in groups_data]
return cls(
id=data["id"],
uid=attributes["uid"],
alias=attributes.get("alias"),
provider=attributes["provider"],
connected=connection_data.get("connected"),
inserted_at=attributes.get("inserted_at"),
updated_at=attributes.get("updated_at"),
last_checked_at=connection_data.get("last_checked_at"),
provider_group_ids=provider_group_ids,
)
class ProvidersListResponse(BaseModel):
"""Simplified response for providers list queries."""
providers: list[SimplifiedProvider]
total_num_providers: int
total_num_pages: int
current_page: int
@classmethod
def from_api_response(cls, response: dict[str, Any]) -> "ProvidersListResponse":
"""Transform JSON:API response to simplified format."""
data = response["data"]
meta = response["meta"]
pagination = meta["pagination"]
providers = [SimplifiedProvider.from_api_response(item) for item in data]
return cls(
providers=providers,
total_num_providers=pagination["count"],
total_num_pages=pagination["pages"],
current_page=pagination["page"],
)
class ProviderConnectionStatus(MinimalSerializerMixin, BaseModel):
"""Result of provider connection operation."""
provider: DetailedProvider
connected: Literal["connected", "failed", "not_tested"]
error: str | None = None
@classmethod
def create(
cls,
provider_data: dict[str, Any],
connection_status: dict[str, Any],
) -> "ProviderConnectionStatus":
"""Create connection status from provider data and connection test result."""
connected: str | None = connection_status.get("connected", None)
if connected is None:
connected = "not_tested"
elif connected:
connected = "connected"
else:
connected = "failed"
return cls(
provider=DetailedProvider.from_api_response(provider_data),
connected=connected,
error=connection_status.get("error", None),
)
@@ -1,137 +0,0 @@
"""Pydantic models for simplified resources responses."""
from prowler_mcp_server.prowler_app.models.base import MinimalSerializerMixin
from pydantic import BaseModel
class SimplifiedResource(MinimalSerializerMixin, BaseModel):
"""Simplified resource with only LLM-relevant information for list operations."""
id: str
uid: str
name: str
region: str
service: str
type: str
failed_findings_count: int
tags: dict[str, str] | None = None
provider_id: str | None = None
@classmethod
def from_api_response(cls, data: dict) -> "SimplifiedResource":
"""Transform JSON:API resource response to simplified format."""
attributes = data["attributes"]
relationships = data.get("relationships", {})
# Extract provider information from relationships if available
provider_id = None
provider_data = relationships.get("provider", {}).get("data", {})
if provider_data:
provider_id = provider_data["id"]
return cls(
id=data["id"],
uid=attributes["uid"],
name=attributes["name"],
region=attributes["region"],
service=attributes["service"],
type=attributes["type"],
failed_findings_count=attributes["failed_findings_count"],
tags=attributes["tags"],
provider_id=provider_id,
)
class DetailedResource(SimplifiedResource):
"""Detailed resource with comprehensive information for deep analysis.
Extends SimplifiedResource with tags, metadata, configuration details,
temporal information, and relationships.
Use this when you need complete context about a specific resource.
"""
metadata: str | None = None
partition: str | None = None
inserted_at: str
updated_at: str
finding_ids: list[str] | None = None
@classmethod
def from_api_response(cls, data: dict) -> "DetailedResource":
"""Transform JSON:API resource response to detailed format."""
attributes = data["attributes"]
relationships = data.get("relationships", {})
# Parse findings relationship
finding_ids = None
findings_data = relationships.get("findings", {}).get("data", [])
if findings_data:
finding_ids = [f["id"] for f in findings_data]
# Extract provider information from relationships if available
provider_id = None
provider_data = relationships.get("provider", {}).get("data", {})
if provider_data:
provider_id = provider_data["id"]
return cls(
id=data["id"],
uid=attributes["uid"],
name=attributes["name"],
region=attributes["region"],
service=attributes["service"],
type=attributes["type"],
failed_findings_count=attributes["failed_findings_count"],
tags=attributes["tags"],
metadata=attributes["metadata"],
partition=attributes["partition"],
inserted_at=attributes["inserted_at"],
updated_at=attributes["updated_at"],
finding_ids=finding_ids,
provider_id=provider_id,
)
class ResourcesListResponse(BaseModel):
"""Simplified response for resources list queries."""
resources: list[SimplifiedResource]
total_num_resources: int
total_num_pages: int
current_page: int
@classmethod
def from_api_response(cls, response: dict) -> "ResourcesListResponse":
"""Transform JSON:API response to simplified format."""
data = response["data"]
meta = response["meta"]
pagination = meta["pagination"]
resources = [SimplifiedResource.from_api_response(item) for item in data]
return cls(
resources=resources,
total_num_resources=pagination["count"],
total_num_pages=pagination["pages"],
current_page=pagination["page"],
)
class ResourcesMetadataResponse(BaseModel):
"""Metadata response with unique filter values for resource discovery."""
services: list[str] | None = None
regions: list[str] | None = None
types: list[str] | None = None
@classmethod
def from_api_response(cls, response: dict) -> "ResourcesMetadataResponse":
"""Transform JSON:API metadata response to simplified format."""
data = response["data"]
attributes = data["attributes"]
return cls(
services=attributes.get("services"),
regions=attributes.get("regions"),
types=attributes.get("types"),
)
@@ -1,222 +0,0 @@
"""Data models for Prowler scans.
This module provides Pydantic models for representing Prowler security scans
with two-tier complexity:
- SimplifiedScan: For list operations with essential fields
- DetailedScan: Extends simplified with additional operational fields
All models inherit from MinimalSerializerMixin to exclude None/empty values
for optimal LLM token usage.
"""
from typing import Any, Literal
from prowler_mcp_server.prowler_app.models.base import MinimalSerializerMixin
from pydantic import BaseModel, ConfigDict, Field
class SimplifiedScan(MinimalSerializerMixin, BaseModel):
"""Simplified scan representation for list operations.
Includes core scan fields for efficient overview.
Used by list_scans() tool.
"""
model_config = ConfigDict(frozen=True)
id: str = Field(
description="Unique UUIDv4 identifier for this scan in Prowler database"
)
name: str | None = Field(
default=None,
description="Optional custom name for the scan to help identify it",
)
trigger: Literal["manual", "scheduled"] = Field(
description="How the scan was initiated: 'manual' (user-triggered) or 'scheduled' (automated)"
)
state: Literal[
"available", "scheduled", "executing", "completed", "failed", "cancelled"
] = Field(
description="Current state of the scan: available, scheduled, executing, completed, failed, or cancelled"
)
started_at: str | None = Field(
default=None, description="ISO 8601 timestamp when the scan started execution"
)
completed_at: str | None = Field(
default=None,
description="ISO 8601 timestamp when the scan finished (completed or failed)",
)
provider_id: str = Field(
description="UUIDv4 identifier of the provider this scan is associated with"
)
@classmethod
def from_api_response(cls, data: dict[str, Any]) -> "SimplifiedScan":
"""Transform JSON:API scan response to simplified model.
Args:
data: Scan data from API response['data'] (single item or list item)
Returns:
SimplifiedScan instance
"""
attributes = data["attributes"]
relationships = data.get("relationships", {})
provider_id = relationships.get("provider", {}).get("data", {}).get("id", None)
return cls(
id=data["id"],
name=attributes.get("name"),
trigger=attributes["trigger"],
state=attributes["state"],
started_at=attributes.get("started_at"),
completed_at=attributes.get("completed_at"),
provider_id=provider_id,
)
class DetailedScan(SimplifiedScan):
"""Detailed scan representation with full operational data.
Extends SimplifiedScan with progress, duration, resources, and relationships.
Used by get_scan() and create_scan() tools.
"""
model_config = ConfigDict(frozen=True)
progress: int | None = Field(
default=None, description="Scan completion progress as percentage (0-100)"
)
duration: int | None = Field(
default=None,
description="Total scan duration in seconds from start to completion",
)
unique_resource_count: int | None = Field(
default=None,
description="Number of unique cloud resources discovered during the scan",
)
inserted_at: str | None = Field(
default=None,
description="ISO 8601 timestamp when the scan was created in the database",
)
scheduled_at: str | None = Field(
default=None,
description="ISO 8601 timestamp when the scan was scheduled to run",
)
next_scan_at: str | None = Field(
default=None,
description="ISO 8601 timestamp for the next scheduled scan (for recurring scans)",
)
@classmethod
def from_api_response(cls, data: dict[str, Any]) -> "DetailedScan":
"""Transform JSON:API scan response to detailed model.
Args:
data: Scan data from API response['data']
Returns:
DetailedScan instance with all fields populated
"""
attributes = data["attributes"]
relationships = data.get("relationships", {})
# Extract provider ID from relationship
provider_rel = relationships.get("provider", {}).get("data", {})
provider_id = provider_rel.get("id", "")
# Extract task relationship
task_rel = relationships.get("task", {}).get("data")
task_id = task_rel.get("id") if task_rel else None
# Extract processor relationship
processor_rel = relationships.get("processor", {}).get("data")
processor_id = processor_rel.get("id") if processor_rel else None
return cls(
id=data["id"],
name=attributes.get("name"),
trigger=attributes["trigger"],
state=attributes["state"],
started_at=attributes.get("started_at"),
completed_at=attributes.get("completed_at"),
provider_id=provider_id,
progress=attributes.get("progress"),
duration=attributes.get("duration"),
unique_resource_count=attributes.get("unique_resource_count"),
inserted_at=attributes.get("inserted_at"),
scheduled_at=attributes.get("scheduled_at"),
next_scan_at=attributes.get("next_scan_at"),
task_id=task_id,
processor_id=processor_id,
)
class ScansListResponse(BaseModel):
"""Response model for list_scans() with pagination metadata.
Follows established pattern from FindingsListResponse and ProvidersListResponse.
"""
scans: list[SimplifiedScan]
total_num_scans: int
total_num_pages: int
current_page: int
@classmethod
def from_api_response(cls, response: dict[str, Any]) -> "ScansListResponse":
"""Transform JSON:API list response to scans list with pagination.
Args:
response: Full API response with data and meta
Returns:
ScansListResponse with simplified scans and pagination metadata
"""
data = response.get("data", [])
meta = response.get("meta", {})
pagination = meta.get("pagination", {})
# Transform each scan
scans = [SimplifiedScan.from_api_response(item) for item in data]
return cls(
scans=scans,
total_num_scans=pagination.get("count", 0),
total_num_pages=pagination.get("pages", 0),
current_page=pagination.get("page", 1),
)
class ScanCreationResult(MinimalSerializerMixin, BaseModel):
"""Result of scan creation operation.
Used by trigger_scan() to communicate the outcome of scan creation.
Status indicates whether scan was created successfully or failed.
"""
scan: DetailedScan | None = Field(
default=None,
description="Detailed scan information if creation succeeded, None otherwise",
)
status: Literal["success", "failed"] = Field(
description="Outcome of scan creation: success (scan created successfully) or failed (error)"
)
message: str = Field(
description="Human-readable message describing the scan creation result"
)
class ScheduleCreationResult(MinimalSerializerMixin, BaseModel):
"""Result of async schedule creation operation.
Used by schedule_daily_scan() to communicate scheduling outcome.
"""
scheduled: bool = Field(
description="Whether the daily scan schedule was created successfully"
)
message: str = Field(
description="Human-readable message describing the scheduling result"
)
@@ -19,9 +19,9 @@ class FindingsTools(BaseTool):
"""Tools for security findings operations.
Provides tools for:
- search_security_findings: Fast and lightweight searching across findings
- get_finding_details: Get complete details for a specific finding
- get_findings_overview: Get aggregate statistics and trends across all findings
- Searching and filtering security findings
- Getting detailed finding information
- Viewing findings overview/statistics
"""
async def search_security_findings(
@@ -90,27 +90,27 @@ class FindingsTools(BaseTool):
) -> dict[str, Any]:
"""Search and filter security findings across all cloud providers with rich filtering capabilities.
IMPORTANT: This tool returns LIGHTWEIGHT findings. Use this for fast searching and filtering across many findings.
For complete details use prowler_app_get_finding_details on specific findings.
This is the primary tool for browsing and filtering security findings. Returns lightweight findings
optimized for searching across large result sets. For detailed information about a specific finding,
use get_finding_details.
Default behavior:
- Returns latest findings from most recent scans (no date parameters needed)
- Filters to FAIL status only (security issues found)
- Returns 50 results per page
- Returns 100 results per page
Date filtering:
- Without dates: queries findings from the most recent completed scan across all providers (most efficient)
- With dates: queries historical findings (2-day maximum range between date_from and date_to)
- Without dates: queries findings from the most recent completed scan across all providers (most efficient). This returns the latest snapshot of findings, not a time-based query.
- With dates: queries historical findings (2-day maximum range)
Each finding includes:
- Core identification: id (UUID for get_finding_details), uid, check_id
- Security context: status (FAIL/PASS/MANUAL), severity (critical/high/medium/low/informational)
- State tracking: delta (new/changed/unchanged), muted (boolean), muted_reason
- Extended details: status_extended with additional context
- Core identification: id, uid, check_id
- Security context: status, severity, check_metadata (title, description, remediation)
- State tracking: delta (new/changed), muted status
- Extended details: status_extended for additional context
Workflow:
1. Use this tool to search and filter findings by severity, status, provider, service, region, etc.
2. Use prowler_app_get_finding_details with the finding 'id' to get complete information about the finding
Returns:
Paginated list of simplified findings with total count and pagination metadata
"""
# Validate page_size parameter
self.api_client.validate_page_size(page_size)
@@ -185,39 +185,21 @@ class FindingsTools(BaseTool):
) -> dict[str, Any]:
"""Retrieve comprehensive details about a specific security finding by its ID.
IMPORTANT: This tool returns COMPLETE finding details.
Use this after finding a specific finding via prowler_app_search_security_findings
This tool provides MORE detailed information than search_security_findings. Use this when you need
to deeply analyze a specific finding or understand its complete context and history.
This tool provides ALL information that prowler_app_search_security_findings returns PLUS:
1. Check Metadata (information about the check script that generated the finding):
- title: Human-readable phrase used to summarize the check
- description: Detailed explanation of what the check validates and why it is important
- risk: What could happen if this check fails
- remediation: Complete remediation guidance including step-by-step instructions and code snippets with best practices to fix the issue:
* cli: Command-line commands to fix the issue
* terraform: Terraform code snippets with best practices
* nativeiac: Provider native Infrastructure as Code code snippets with best practices to fix the issue
* other: Other remediation code snippets with best practices, usually used for web interfaces or other tools
* recommendation: Text description with general best recommended practices to avoid the issue
- provider: Cloud provider (aws/azure/gcp/etc)
- service: Service name (s3/ec2/keyvault/etc)
- resource_type: Resource type being evaluated
- categories: Security categories this check belongs to
- additional_urls: List of additional URLs related to the check
2. Temporal Metadata:
- inserted_at: When this finding was first inserted into database
- updated_at: When this finding was last updated
- first_seen_at: When this finding was first detected across all scans
3. Relationships:
- scan_id: UUID of the scan that generated this finding
- resource_ids: List of UUIDs for cloud resources associated with this finding
Additional information compared to search_security_findings:
- Temporal metadata: when the finding was first seen, inserted, and last updated
- Scan relationship: ID of the scan that generated this finding
- Resource relationships: IDs of all cloud resources associated with this finding
Workflow:
1. Use prowler_app_search_security_findings to browse and filter findings
2. Use this tool with the finding 'id' to get remediation guidance and complete context
1. Use search_security_findings to browse and filter across many findings
2. Use get_finding_details to drill down into specific findings of interest
Returns:
dict containing detailed finding with comprehensive security metadata, temporal information,
and relationships to scans and resources
"""
params = {
# Return comprehensive fields including temporal metadata
@@ -243,31 +225,26 @@ class FindingsTools(BaseTool):
description="Filter statistics by cloud provider. Multiple values allowed. If empty, all providers are returned. For valid values, please refer to Prowler Hub/Prowler Documentation that you can also find in form of tools in this MCP Server.",
),
) -> dict[str, Any]:
"""Get aggregate statistics and trends about security findings as a markdown report.
"""Get high-level statistics about security findings formatted as a human-readable markdown report.
This tool provides a HIGH-LEVEL OVERVIEW without retrieving individual findings. Use this when you
need to understand the overall security posture, trends, or remediation progress across all findings.
Use this tool to get a quick overview of your security posture without retrieving individual findings.
Perfect for understanding trends, identifying areas of concern, and tracking improvements over time.
The markdown report includes:
The report includes:
- Summary statistics: total findings, fail/pass/muted counts with percentages
- Delta analysis: breakdown of new vs changed findings
- Trending information: how findings are evolving over time
1. Summary Statistics:
- Total number of findings
- Failed checks (security issues) with percentage
- Passed checks (no issues) with percentage
- Muted findings (user-suppressed) with percentage
Output format: Markdown-formatted report ready to present to users or include in documentation.
2. Delta Analysis (Change Tracking):
- New findings: never seen before in previous scans
* Broken down by: new failures, new passes, new muted
- Changed findings: status changed since last scan
* Broken down by: changed to fail, changed to pass, changed to muted
- Unchanged findings: same status as previous scan
Use cases:
- Quick security posture assessment
- Tracking remediation progress over time
- Identifying which providers have most issues
- Understanding finding trends (improving or degrading)
This helps answer questions like:
- "What's my overall security posture?"
- "How many critical security issues do I have?"
- "Are we improving or getting worse over time?"
- "How many new security issues appeared since last scan?"
Returns:
Dictionary with 'report' key containing markdown-formatted summary statistics
"""
params = {
# Return only LLM-relevant aggregate statistics
@@ -1,477 +0,0 @@
"""Muting tools for Prowler App MCP Server.
This module provides tools for managing finding muting in Prowler, including:
- Mutelist management (pattern-based bulk muting)
- Mute rules management (finding-specific muting)
"""
import json
from typing import Any
from prowler_mcp_server.prowler_app.models.muting import (
DetailedMuteRule,
MutelistResponse,
MuteRulesListResponse,
)
from prowler_mcp_server.prowler_app.tools.base import BaseTool
from pydantic import Field
class MutingTools(BaseTool):
"""Tools for muting operations.
Provides tools for:
- Managing mutelist (pattern-based bulk muting)
- Managing mute rules (finding-specific muting)
"""
# ===== MUTELIST TOOLS =====
async def get_mutelist(self) -> dict[str, Any]:
"""Retrieve the current mutelist configuration for the tenant.
IMPORTANT: Only one mutelist can exist per tenant. Returns an error message if no mutelist exists.
For detailed information about mutelist structure and configuration, search Prowler documentation
using prowler_docs_search tool available in this MCP Server.
The mutelist includes:
- Core identification: id (UUID for processor operations)
- Configuration: Nested structure with Accounts → Checks → Regions/Resources/Tags/Exceptions patterns
- Temporal data: inserted_at, updated_at timestamps
Workflow:
1. Use this tool to check if a mutelist is configured
2. Examine current muting patterns before making updates
3. Use prowler_app_set_mutelist to create or update the configuration
"""
self.logger.info("Retrieving mutelist configuration...")
# Query processors filtered by type=mutelist
params = {
"filter[processor_type]": "mutelist",
"fields[processors]": "processor_type,configuration,inserted_at,updated_at",
}
clean_params = self.api_client.build_filter_params(params)
api_response = await self.api_client.get(
"/api/v1/processors", params=clean_params
)
data = api_response.get("data", [])
if len(data) == 0:
return {
"error": "No mutelist found",
"message": "No mutelist configuration exists for this tenant. Use prowler_app_set_mutelist to create one.",
}
# Return the first (and only) mutelist
mutelist = MutelistResponse.from_api_response(data[0])
return mutelist.model_dump()
async def set_mutelist(
self,
configuration: dict[str, Any] | str = Field(
description="""Mutelist configuration object following the Accounts/Checks/Regions/Resources/Tags/Exceptions structure.
Accepts either a dictionary or JSON string. The configuration replaces the entire mutelist (not merged with existing).
Structure:
{
"Mutelist": {
"Accounts": {
"<account-pattern>": { // "*" for all accounts, or specific account ID
"Checks": {
"<check-id>": { // Prowler check ID
"Regions": ["us-east-1", "eu-west-1"], // Optional
"Resources": ["arn:aws:s3:::my-bucket"], // Optional
"Tags": ["Environment:dev"], // Optional
"Exceptions": { // Optional
"Accounts": ["123456789012"],
"Regions": ["us-west-2"],
"Resources": ["arn:aws:s3:::critical-bucket"]
}
}
}
}
}
}
}"""
),
) -> dict[str, Any]:
"""Create or update the mutelist configuration for pattern-based bulk muting.
IMPORTANT: Automatically creates a new mutelist or updates the existing one (only one mutelist per tenant).
The configuration completely replaces any existing mutelist (not merged).
For detailed information about mutelist structure and configuration, search Prowler documentation
using prowler_docs_search tool available in this MCP Server.
Default behavior:
- Creates new mutelist if none exists
- Updates existing mutelist with complete replacement
- Applies to findings from future scans
The mutelist supports:
- Account patterns: Specific account IDs or "*" for all
- Check-based muting: Per-check ID configuration
- Scope filtering: Regions, Resources, Tags
- Exceptions: Accounts, Regions, Resources to exclude from muting
Workflow:
1. Use prowler_app_get_mutelist to check existing configuration
2. Build configuration object following Prowler mutelist format
3. Use this tool to create or update the mutelist
4. Verify with prowler_app_get_mutelist
"""
self.logger.info("Setting mutelist configuration...")
# Parse configuration if it's a string
if isinstance(configuration, str):
configuration = json.loads(configuration)
# Check if mutelist already exists
existing_mutelist = await self.get_mutelist()
if "error" in existing_mutelist:
# Create new mutelist
self.logger.info("Creating new mutelist...")
create_body = {
"data": {
"type": "processors",
"attributes": {
"processor_type": "mutelist",
"configuration": configuration,
},
}
}
api_response = await self.api_client.post(
"/api/v1/processors", json_data=create_body
)
mutelist = MutelistResponse.from_api_response(api_response.get("data", {}))
return mutelist.model_dump()
else:
# Update existing mutelist
self.logger.info(f"Updating existing mutelist {existing_mutelist['id']}...")
update_body = {
"data": {
"type": "processors",
"id": existing_mutelist["id"],
"attributes": {
"configuration": configuration,
},
}
}
api_response = await self.api_client.patch(
f"/api/v1/processors/{existing_mutelist['id']}", json_data=update_body
)
mutelist = MutelistResponse.from_api_response(api_response.get("data", {}))
return mutelist.model_dump()
async def delete_mutelist(self) -> dict[str, Any]:
"""Remove the mutelist configuration from the tenant.
WARNING: This is a destructive operation that cannot be undone.
- The mutelist will need to be re-created with prowler_app_set_mutelist
- New findings from future scans will NOT be muted by the deleted mutelist
- Previously muted findings remain muted (deletion doesn't un-mute them)
Workflow:
1. Use prowler_app_get_mutelist to confirm what will be deleted
2. Use this tool to permanently remove the mutelist
3. New scans will no longer apply mutelist-based muting
"""
self.logger.info("Deleting mutelist configuration...")
# Get existing mutelist
existing_mutelist = await self.get_mutelist()
if "error" in existing_mutelist:
return {
"success": False,
"message": "No mutelist found to delete",
}
# Delete the mutelist
mutelist_id = existing_mutelist["id"]
await self.api_client.delete(f"/api/v1/processors/{mutelist_id}")
return {
"success": True,
"message": "Mutelist deleted successfully",
}
# ===== MUTE RULES TOOLS =====
async def list_mute_rules(
self,
name: str | None = Field(
default=None,
description="Filter by exact rule name",
),
enabled: (
bool | str | None
) = Field( # Wrong `str` hint type due to bad MCP Clients implementation
default=None,
description="Filter by enabled status. True for enabled rules only, False for disabled rules only. If not specified, returns both enabled and disabled rules. Strings 'true' and 'false' are also accepted.",
),
search: str | None = Field(
default=None,
description="Free-text search term across multiple fields (name, reason). Use this for general keyword search.",
),
page_size: int = Field(
default=50, description="Number of results to return per page."
),
page_number: int = Field(
default=1,
description="Page number to retrieve (1-indexed)",
),
) -> dict[str, Any]:
"""Search and filter mute rules with pagination support.
IMPORTANT: This tool returns LIGHTWEIGHT mute rules without the full list of finding UIDs.
Use prowler_app_get_mute_rule to get complete details including all finding UIDs and creator information.
Default behavior:
- Returns all mute rules (both enabled and disabled)
- Returns 50 rules per page
- Includes basic rule information without full finding UID lists
Each mute rule includes:
- Core identification: id (UUID for prowler_app_get_mute_rule), name
- Contextual information: reason, enabled status
- State tracking: finding_count (number of findings currently muted)
- Temporal data: inserted_at, updated_at timestamps
Workflow:
1. Use this tool to search and filter mute rules by name, enabled status, or keywords
2. Use prowler_app_get_mute_rule with the mute rule 'id' to get complete details including all finding UIDs
3. Use prowler_app_update_mute_rule or prowler_app_delete_mute_rule to modify rules
"""
self.logger.info("Listing mute rules...")
self.api_client.validate_page_size(page_size)
params = {
"fields[mute-rules]": "name,reason,enabled,finding_uids,inserted_at,updated_at",
"page[size]": page_size,
"page[number]": page_number,
}
# Build filter parameters
if name:
params["filter[name]"] = name
if enabled is not None:
if isinstance(enabled, bool):
params["filter[enabled]"] = enabled
else:
if enabled.lower() == "true":
params["filter[enabled]"] = True
elif enabled.lower() == "false":
params["filter[enabled]"] = False
else:
raise ValueError(
f"Invalid enabled value: {enabled}. Valid values are True, False, 'true', 'false' or None."
)
if search:
params["filter[search]"] = search
clean_params = self.api_client.build_filter_params(params)
api_response = await self.api_client.get(
"/api/v1/mute-rules", params=clean_params
)
simplified_response = MuteRulesListResponse.from_api_response(api_response)
return simplified_response.model_dump()
async def get_mute_rule(
self,
rule_id: str = Field(
description="UUID of the mute rule to retrieve. Must be a valid UUID format (e.g., '019ac0d6-90d5-73e9-9acf-c22e256f1bac')."
),
) -> dict[str, Any]:
"""Retrieve comprehensive details about a specific mute rule by its ID.
IMPORTANT: This tool returns COMPLETE mute rule details including the full list of finding UIDs.
Use this after finding a rule via prowler_app_list_mute_rules.
This tool provides ALL information that prowler_app_list_mute_rules returns PLUS:
- finding_uids: Complete list of finding UIDs that are muted by this rule
- user_creator_id: UUID of the user who created the rule (audit trail)
Workflow:
1. Use prowler_app_list_mute_rules to find rules by name or filter criteria
2. Use this tool with the rule 'id' to get complete details
3. Examine finding_uids list to understand which findings are muted
4. Use prowler_app_update_mute_rule or prowler_app_delete_mute_rule to modify if needed
"""
self.logger.info(f"Retrieving mute rule {rule_id}...")
params = {
"include": "created_by",
}
api_response = await self.api_client.get(
f"/api/v1/mute-rules/{rule_id}", params=params
)
detailed_rule = DetailedMuteRule.from_api_response(api_response.get("data", {}))
return detailed_rule.model_dump()
async def create_mute_rule(
self,
name: str = Field(
description="Name for the mute rule. Should be descriptive and meaningful (e.g., 'Dev S3 Public Access', 'Test Environment IMDSv1')."
),
reason: str = Field(
description="Reason for muting these findings. Document why this security issue is acceptable or intentional (e.g., 'Development environment with controlled access', 'Legacy application requires IMDSv1')."
),
finding_ids: list[str] = Field(
description="List of finding IDs (UUIDs) to mute. Get these from the prowler_app_search_security_findings tool. Must provide at least 1 finding ID."
),
) -> dict[str, Any]:
"""Create a new mute rule to mute specific findings with documentation and audit trail.
IMPORTANT: This immediately mutes the specified findings AND all previous findings with matching UIDs (this could take some time to complete).
The rule is enabled by default. Muting is permanent.
Default behavior:
- Rule is created in enabled state
- Applies to current and previous findings with matching UIDs
- Records creator for audit trail
The mute rule includes:
- Core identification: id (UUID for prowler_app_get_mute_rule), name, reason
- Configuration: enabled status, finding_uids list
- Audit trail: user_creator_id (UUID of the Prowler user from the tenant that created the rule), timestamps when the rule was created and last modified
Workflow:
1. Use prowler_app_search_security_findings to identify findings to mute
2. Use this tool with finding IDs, descriptive name, and documented reason
3. Verify with prowler_app_get_mute_rule to confirm rule creation
4. Check findings are muted with prowler_app_search_security_findings (filter by muted=true)
"""
self.logger.info(f"Creating mute rule '{name}'...")
create_body = {
"data": {
"type": "mute-rules",
"attributes": {
"name": name,
"reason": reason,
"finding_ids": finding_ids,
},
}
}
api_response = await self.api_client.post(
"/api/v1/mute-rules", json_data=create_body
)
detailed_rule = DetailedMuteRule.from_api_response(api_response.get("data", {}))
return detailed_rule.model_dump()
async def update_mute_rule(
self,
rule_id: str = Field(
description="UUID of the mute rule to update. Must be a valid UUID format."
),
name: str | None = Field(
default=None,
description="New name for the rule. If not specified, name remains unchanged.",
),
reason: str | None = Field(
default=None,
description="New reason for the rule. If not specified, reason remains unchanged.",
),
enabled: bool | None = Field(
default=None,
description="Enable (True) or disable (False) the rule. If not specified, enabled status remains unchanged. IMPORTANT: Disabling a rule does not un-mute findings - they remain muted.",
),
) -> dict[str, Any]:
"""Update a mute rule's name, reason, or enabled status.
IMPORTANT: Cannot change which findings are muted (finding_uids are immutable).
Disabling a rule does NOT un-mute findings - they remain muted permanently.
Default behavior:
- Only specified fields are updated
- Unspecified fields remain unchanged
- If no parameters provided, returns current rule state
Updatable fields:
- name: Change rule name for better organization
- reason: Update documentation/justification
- enabled: Toggle rule active status (doesn't affect already-muted findings)
Workflow:
1. Use prowler_app_get_mute_rule to see current rule state
2. Use this tool to update name, reason, or enabled status
3. Verify changes with prowler_app_get_mute_rule
"""
self.logger.info(f"Updating mute rule {rule_id}...")
# Build update body with only provided fields
attributes = {}
if name is not None:
attributes["name"] = name
if reason is not None:
attributes["reason"] = reason
if enabled is not None:
attributes["enabled"] = enabled
if not attributes:
# No updates provided, just return current state
return await self.get_mute_rule(rule_id)
update_body = {
"data": {
"type": "mute-rules",
"id": rule_id,
"attributes": attributes,
}
}
api_response = await self.api_client.patch(
f"/api/v1/mute-rules/{rule_id}", json_data=update_body
)
self.logger.info(f"API response: {api_response}")
detailed_rule = DetailedMuteRule.from_api_response(api_response.get("data", {}))
return detailed_rule.model_dump()
async def delete_mute_rule(
self,
rule_id: str = Field(
description="UUID of the mute rule to delete. Must be a valid UUID format."
),
) -> dict[str, Any]:
"""Delete a mute rule from the system.
WARNING: Findings that were muted by this rule REMAIN MUTED after deletion.
This only removes the rule itself from management, not the muting effect on findings.
The muted findings will stay muted permanently.
Deletion behavior:
- Rule is permanently removed from the system
- Muted findings remain muted (deletion doesn't un-mute them)
- Cannot be undone - rule must be recreated to restore
Workflow:
1. Use prowler_app_get_mute_rule to review what will be deleted
2. Use this tool to permanently remove the rule
3. Verify deletion with prowler_app_list_mute_rules (rule should no longer appear)
"""
self.logger.info(f"Deleting mute rule {rule_id}...")
result = await self.api_client.delete(f"/api/v1/mute-rules/{rule_id}")
if result.get("success"):
return {
"success": True,
"message": "Mute rule deleted successfully",
}
else:
return {
"success": False,
"message": "Failed to delete mute rule",
}
@@ -1,623 +0,0 @@
"""Provider Management tools for Prowler App MCP Server.
This module provides tools for managing provider connections,
including searching, connecting, and deleting providers.
"""
from typing import Any
from prowler_mcp_server.prowler_app.models.providers import (
ProviderConnectionStatus,
ProvidersListResponse,
)
from prowler_mcp_server.prowler_app.tools.base import BaseTool
from pydantic import Field
class ProvidersTools(BaseTool):
"""Tools for provider management operations
Provides tools for:
- prowler_app_search_providers: Search and view configured providers with their connection status
- prowler_app_connect_provider: Connect or register a provider for security scanning in Prowler
- prowler_app_delete_provider: Permanently remove a provider from Prowler
"""
async def search_providers(
self,
provider_id: list[str] = Field(
default=[],
description="Filter by Prowler's internal UUID(s) (v4) for the provider(s), generated when the provider is registered in the system.",
),
provider_uid: list[str] = Field(
default=[],
description="Filter by provider's unique identifier(s), this ID is the one provided by the provider itself. Format varies by provider type: AWS Account ID (12 digits), Azure Subscription ID (UUID), GCP Project ID (string), Kubernetes namespace, GitHub username/organization, M365 domain ID, etc. All supported provider types are listed in the Prowler Hub/Prowler Documentation that you can also find in form of tools in this MCP Server",
),
provider_type: list[str] = Field(
default=[],
description="Filter by provider type. Valid values include: 'aws', 'azure', 'gcp', 'kubernetes'... For more valid values, please refer to Prowler Hub/Prowler Documentation that you can also find in form of tools in this MCP Server.",
),
alias: str | None = Field(
default=None,
description="Search by provider alias/friendly name. Partial match supported (case-insensitive). Use this to find providers by their human-readable name (e.g., 'Production', 'Dev', 'AWS Main')",
),
connected: (
bool | str | None
) = Field( # Wrong `str` hint type due to bad MCP Clients implementation
default=None,
description="Filter by connection status. True returns only successfully connected providers (credentials work), False returns only providers with failed connections (credentials invalid). If not specified, returns all connected, failed and not tested providers. Strings 'true' and 'false' are also accepted.",
),
page_size: int = Field(
default=50, description="Number of results to return per page"
),
page_number: int = Field(
default=1,
description="Page number to retrieve (1-indexed)",
),
) -> dict[str, Any]:
"""Search and view configured providers to be scanned with Prowler.
This tool returns a unified view of all providers configured in Prowler.
For getting more details about what types of providers are available to be scanned with Prowler or
what are the UIDs are accepted for each provider type, please refer to Prowler Hub/Prowler Documentation
that you can also find in form of tools in this MCP Server.
Each provider includes:
- Provider identification: Prowler Internal ID, External Provider UID, Provider Alias
- Provider context: Provider Type
- Connection status: Connected (true), Failed (false), Not Tested (null)
"""
self.api_client.validate_page_size(page_size)
params = {
"fields[providers]": "uid,alias,provider,connection,secret",
"page[number]": page_number,
"page[size]": page_size,
}
# Build filter parameters
if provider_id:
params["filter[id__in]"] = provider_id
if provider_uid:
params["filter[uid__in]"] = provider_uid
if provider_type:
params["filter[provider__in]"] = provider_type
if alias:
params["filter[alias__icontains]"] = alias
if connected is not None:
if isinstance(connected, bool):
params["filter[connected]"] = connected
else:
if connected.lower() == "true":
params["filter[connected]"] = True
elif connected.lower() == "false":
params["filter[connected]"] = False
else:
raise ValueError(
f"Invalid connected value: {connected}. Valid values are True, False, 'true', 'false' or None."
)
clean_params = self.api_client.build_filter_params(params)
api_response = await self.api_client.get(
"/api/v1/providers", params=clean_params
)
simplified_response = ProvidersListResponse.from_api_response(api_response)
# Fetch secret_type for each provider that has a secret
for provider in simplified_response.providers:
# Get the provider data from the API response to access relationships
provider_data = next(
(
provider_api_response
for provider_api_response in api_response["data"]
if provider_api_response["id"] == provider.id
),
None,
)
if provider_data:
secret_relationship = provider_data.get("relationships", {}).get(
"secret", {}
)
secret_data = secret_relationship.get("data")
if secret_data:
secret_id = secret_data["id"]
provider.secret_type = await self._get_secret_type(secret_id)
return simplified_response.model_dump()
async def connect_provider(
self,
provider_uid: str = Field(
description="Provider's unique identifier. For supported UID provider formats, please refer to Prowler Hub/Prowler Documentation that you can also find in form of tools in this MCP Server"
),
provider_type: str = Field(
description="Type of provider to be scanned with Prowler. Valid values include: 'aws', 'azure', 'gcp', 'kubernetes'... For more valid values, please refer to Prowler Hub/Prowler Documentation that you can also find in form of tools in this MCP Server."
),
alias: str | None = Field(
default=None,
description="Human-friendly name for this provider. Optional but recommended for easy identification. Use descriptive names to distinguish multiple accounts of the same type.",
),
credentials: dict[str, Any] | None = Field(
default=None,
description="Provider-specific credentials for authentication. Optional - if not provided, provider is created but not connected. Structure varies by provider type. For supported provider types, please refer to Prowler Hub/Prowler Documentation that you can also find in form of tools in this MCP Server",
),
) -> dict[str, Any]:
"""Register a provider to be scanned with Prowler.
This tool will register a provider in Prowler App, even if the UID is wrong.
If the provider is already registered, it will be updated with the new provided alias or credentials if provided.
If credentials are provided, they will be added to the indicated provider, if the provider does not exist, it will be created and the credentials will be added to it.
If the connection test is successful, the provider will be connected.
If the connection test fails, the provider will be created but not connected.
The tool always returns the provider details after its registration or update.
Example Input:
- AWS Static Credentials:
```json
{
"provider_uid": "123456789012",
"provider_type": "aws",
"alias": "production-aws-account",
"credentials": {
"aws_access_key_id": "AKIA...",
"aws_secret_access_key": "...",
"aws_session_token": "..."
}
}
```
- AWS Assume Role:
```json
{
"provider_uid": "987654321098",
"provider_type": "aws",
"alias": "staging-aws-account",
"credentials": {
"role_arn": "arn:aws:iam::987654321098:role/ProwlerScanRole",
"external_id": "...",
"aws_access_key_id": "AKIA...", # Optional
"aws_secret_access_key": "...", # Optional
"aws_session_token": "...", # Optional
"session_duration": 3600, # Optional
"role_session_name": "..." # Optional
}
}
```
- Azure/M365 Static Credentials:
```json
{
"provider_uid": "a1b2c3d4-e5f6-4a5b-8c9d-0e1f2a3b4c5d",
"provider_type": "azure",
"alias": "production-azure-subscription",
"credentials": {
"client_id": "...",
"client_secret": "...",
"tenant_id": "..."
}
}
```
- GCP Service Account Account Key:
```json
{
"provider_uid": "my-gcp-project-prod",
"provider_type": "gcp",
"alias": "production-gcp-project",
"credentials": {
"service_account_key": {
"type": "service_account",
"project_id": "...",
"private_key_id": "...",
"private_key": "...",
"client_email": "...",
}
}
}
```
- Kubernetes Static Credentials:
```json
{
"provider_uid": "prod-k8s-cluster",
"provider_type": "kubernetes",
"alias": "production-kubernetes-cluster",
"credentials": {
"kubeconfig_content": "..."
}
}
```
- GitHub OAuth App Token:
```json
{
"provider_uid": "my-organization",
"provider_type": "github",
"alias": "my-github-organization",
"credentials": {
"oauth_app_token": "..."
}
}
NOTE: THERE ARE MORE PROVIDER TYPES AND CREDENTIAL TYPES AVAILABLE, PLEASE REFER TO THE Prowler Hub/Prowler Documentation that you can also find in form of tools in this MCP Server.
"""
# Step 1: Check if provider already exists
prowler_provider_id = await self._check_provider_exists(provider_uid)
# Step 2: Create or update provider
if prowler_provider_id is None:
prowler_provider_id = await self._create_provider(
provider_uid, provider_type, alias
)
elif alias:
await self._update_provider_alias(prowler_provider_id, alias)
# Step 3: Handle credentials if provided and capture secret response
secret_response = None
if credentials:
secret_response = await self._store_credentials(
prowler_provider_id, credentials
)
# Step 4: Test connection
connection_status = await self._test_connection(prowler_provider_id)
# Step 5: Get final provider state with relationships
final_provider = await self._get_final_provider_state(prowler_provider_id)
# Transform to structured response using model
connection_result = ProviderConnectionStatus.create(
provider_data=final_provider["data"],
connection_status=connection_status,
)
if secret_response:
# We just stored credentials, use the secret_type from the response
connection_result.provider.secret_type = (
secret_response.get("data", {}).get("attributes", {}).get("secret_type")
)
else:
# No new credentials provided, check if provider has an existing secret
secret_data = (
final_provider.get("data", {})
.get("relationships", {})
.get("secret", {})
.get("data")
)
if secret_data:
# Provider has existing secret, fetch its type
secret_id = secret_data["id"]
connection_result.provider.secret_type = await self._get_secret_type(
secret_id
)
return connection_result.model_dump()
async def delete_provider(
self,
provider_id: str = Field(
description="Prowler's internal UUID (v4) for the provider to permanently remove, generated when the provider was registered in the system. Use `prowler_app_search_providers` tool to find the provider_id if you only know the alias or the provider's own identifier (provider_uid)"
),
) -> dict[str, Any]:
"""Permanently remove a registered provider from Prowler.
WARNING: This is a destructive operation that cannot be undone. The provider will need to be
re-added with prowler_app_connect_provider if you want to scan it again.
The tool always returns the deletion status and message.
"""
self.logger.info(f"Deleting provider {provider_id}...")
try:
# Initiate the deletion task
task_response = await self.api_client.delete(
f"/api/v1/providers/{provider_id}"
)
task_id = task_response.get("data", {}).get("id")
# Poll until task completes (with 60 second timeout)
await self.api_client.poll_task_until_complete(
task_id=task_id, timeout=60, poll_interval=1.0
)
# If we reach here, the task completed successfully
return {
"deleted": True,
"message": f"Provider {provider_id} deleted successfully",
}
except Exception as e:
self.logger.error(f"Provider deletion failed: {e}")
return {
"deleted": False,
"message": f"Provider {provider_id} deletion failed: {str(e)}",
}
# Private helper methods
async def _check_provider_exists(self, provider_uid: str) -> str | None:
"""Check if a provider already exists by its UID.
Args:
provider_uid: The provider's unique identifier (e.g., AWS account ID)
Returns:
The Prowler-generated provider ID if exists, None otherwise
Raises:
Exception: If multiple providers with the same UID are found (data integrity issue)
Exception: If API request fails
"""
self.logger.info(f"Checking if provider {provider_uid} exists...")
response = await self.api_client.get(
"/api/v1/providers", params={"filter[uid]": provider_uid}
)
providers = response.get("data", [])
if len(providers) == 0:
self.logger.info(f"Provider {provider_uid} does not exist")
return None
elif len(providers) == 1:
prowler_provider_id = providers[0].get("id")
self.logger.info(
f"Provider {provider_uid} exists with ID {prowler_provider_id}"
)
return prowler_provider_id
else:
# Multiple providers with the same UID is a data integrity issue
raise Exception(
f"Data integrity error: Found {len(providers)} providers with UID '{provider_uid}'. "
f"Each provider UID should be unique. Please contact support or manually clean up duplicate providers."
)
async def _create_provider(
self, provider_uid: str, provider_type: str, alias: str | None
) -> str:
"""Create a new provider.
Args:
provider_uid: The provider's unique identifier
provider_type: Type of provider to be scanned with Prowler (aws, azure, gcp, etc.)
alias: Optional human-friendly name for the provider
Returns:
The provider UID (which is used as the ID)
"""
self.logger.info(f"Creating provider {provider_uid} (type: {provider_type})...")
provider_body = {
"data": {
"type": "providers",
"attributes": {
"uid": provider_uid,
"provider": provider_type,
},
}
}
if alias:
provider_body["data"]["attributes"]["alias"] = alias
await self.api_client.post("/api/v1/providers", json_data=provider_body)
provider_id = await self._check_provider_exists(provider_uid)
if provider_id is None:
raise Exception(f"Provider {provider_uid} creation failed")
return provider_id
async def _update_provider_alias(
self, prowler_provider_id: str, alias: str
) -> None:
"""Update the alias of an existing provider.
Args:
prowler_provider_id: The Prowler-generated provider ID
alias: New human-friendly name for the provider
"""
self.logger.info(f"Updating provider {prowler_provider_id} alias...")
update_body = {
"data": {
"type": "providers",
"id": prowler_provider_id,
"attributes": {
"alias": alias,
},
}
}
result = await self.api_client.patch(
f"/api/v1/providers/{prowler_provider_id}", json_data=update_body
)
if result.get("data", {}).get("attributes", {}).get("alias") != alias:
raise Exception(f"Provider {prowler_provider_id} alias update failed")
def _determine_secret_type(self, credentials: dict[str, Any]) -> str:
"""Determine the secret type from credentials structure.
Args:
credentials: The credentials dictionary
Returns:
Secret type: "role", "service_account", or "static"
"""
if "role_arn" in credentials:
return "role"
elif "service_account_key" in credentials:
return "service_account"
else:
return "static"
async def _get_provider_secret_id(self, prowler_provider_id: str) -> str | None:
"""Get the secret ID for a provider if it exists.
Args:
prowler_provider_id: The Prowler-generated provider ID
Returns:
The secret ID if exists, None otherwise
"""
try:
response = await self.api_client.get(
"/api/v1/providers/secrets",
params={"filter[provider]": prowler_provider_id},
)
secrets = response.get("data", [])
if len(secrets) > 0:
secret_id = secrets[0].get("id")
self.logger.info(
f"Found existing secret {secret_id} for provider {prowler_provider_id}"
)
return secret_id
else:
self.logger.info(
f"No existing secret found for provider {prowler_provider_id}"
)
return None
except Exception as e:
self.logger.error(f"Error checking for existing secret: {e}")
return None
async def _get_secret_type(self, secret_id: str) -> str | None:
"""Get the secret type for a given secret ID.
Args:
secret_id: The secret ID from provider relationships
Returns:
The secret type ("role", "service_account", or "static") if found, None otherwise
"""
try:
response = await self.api_client.get(
f"/api/v1/providers/secrets/{secret_id}",
params={"fields[provider-secrets]": "secret_type"},
)
secret_type = (
response.get("data", {}).get("attributes", {}).get("secret_type")
)
return secret_type
except Exception as e:
self.logger.error(f"Error fetching secret type for {secret_id}: {e}")
return None
async def _store_credentials(
self, prowler_provider_id: str, credentials: dict[str, Any]
) -> dict[str, Any]:
"""Store or update credentials for a provider.
Args:
prowler_provider_id: The Prowler-generated provider ID
credentials: The credentials to store
Returns:
The API response with the secret data
"""
self.logger.info(
f"Adding/updating credentials for provider {prowler_provider_id}..."
)
secret_type = self._determine_secret_type(credentials)
# Check if a secret already exists for this provider
existing_secret_id = await self._get_provider_secret_id(prowler_provider_id)
if existing_secret_id:
# Update existing secret
self.logger.info(f"Updating existing secret {existing_secret_id}...")
update_body = {
"data": {
"type": "provider-secrets",
"id": existing_secret_id,
"attributes": {
"secret_type": secret_type,
"secret": credentials,
},
"relationships": {
"provider": {
"data": {
"type": "providers",
"id": prowler_provider_id,
}
}
},
}
}
try:
response = await self.api_client.patch(
f"/api/v1/providers/secrets/{existing_secret_id}",
json_data=update_body,
)
self.logger.info("Credentials updated successfully")
return response
except Exception as e:
self.logger.error(f"Error updating credentials: {e}")
raise
else:
# Create new secret
self.logger.info("Creating new secret...")
secret_body = {
"data": {
"type": "provider-secrets",
"attributes": {
"secret_type": secret_type,
"secret": credentials,
},
"relationships": {
"provider": {
"data": {
"type": "providers",
"id": prowler_provider_id,
}
}
},
}
}
try:
response = await self.api_client.post(
"/api/v1/providers/secrets", json_data=secret_body
)
self.logger.info("Credentials added successfully")
return response
except Exception as e:
self.logger.error(f"Error adding credentials: {e}")
raise
async def _test_connection(self, prowler_provider_id: str) -> dict[str, Any]:
"""Test connection to a provider.
Args:
prowler_provider_id: The Prowler-generated provider ID
Returns:
Connection status dictionary with 'connected' boolean and optional 'error' message
"""
self.logger.info(f"Testing connection for provider {prowler_provider_id}...")
try:
# Initiate the connection test task
task_response = await self.api_client.post(
f"/api/v1/providers/{prowler_provider_id}/connection", json_data={}
)
task_id = task_response.get("data", {}).get("id")
# Poll until task completes (with 60 second timeout)
completed_task = await self.api_client.poll_task_until_complete(
task_id=task_id, timeout=60, poll_interval=1.0
)
# Extract the result from the completed task
task_result = (
completed_task.get("data", {}).get("attributes", {}).get("result", {})
)
return task_result
except Exception as e:
self.logger.error(f"Connection test failed: {e}")
return {"connected": False, "error": str(e)}
async def _get_final_provider_state(
self, prowler_provider_id: str
) -> dict[str, Any]:
"""Get final provider state with relationships.
Args:
prowler_provider_id: The Prowler-generated provider ID
Returns:
Provider data dictionary
"""
return await self.api_client.get(
f"/api/v1/providers/{prowler_provider_id}",
)
@@ -1,345 +0,0 @@
"""Cloud Resources tools for Prowler App MCP Server.
This module provides tools for searching, viewing, and analyzing cloud resources
across all providers.
"""
from typing import Any
from prowler_mcp_server.prowler_app.models.resources import (
DetailedResource,
ResourcesListResponse,
ResourcesMetadataResponse,
)
from prowler_mcp_server.prowler_app.tools.base import BaseTool
from pydantic import Field
class ResourcesTools(BaseTool):
"""Tools for cloud resources operations.
Provides tools for:
- Searching and filtering cloud resources
- Getting detailed resource information
- Viewing resources overview with statistics
"""
async def list_resources(
self,
provider_type: list[str] = Field(
default=[],
description="Filter by provider type. Multiple values allowed. If empty, all providers are returned. For valid values, please refer to Prowler Hub/Prowler Documentation that you can also find in form of tools in this MCP Server.",
),
provider_alias: str | None = Field(
default=None,
description="Filter by specific provider alias/name (partial match supported). Useful for finding resources in specific accounts like 'production' or 'dev'.",
),
provider_uid: str | None = Field(
default=None,
description="Filter by provider's native ID (e.g., AWS account ID, Azure subscription ID, GCP project ID). All supported provider types are listed in the Prowler Hub/Prowler Documentation that you can also find in form of tools in this MCP Server",
),
region: list[str] = Field(
default=[],
description="Filter by regions. Multiple values allowed (e.g., us-east-1, westus2, europe-west1), format may vary depending on the provider. If empty, all regions are returned.",
),
service: list[str] = Field(
default=[],
description="Filter by service. Multiple values allowed (e.g., s3, ec2, iam, keyvault). If empty, all services are returned.",
),
resource_type: list[str] = Field(
default=[],
description="Filter by resource type. Format may vary depending on the provider. If empty, all resource types are returned.",
),
resource_name: str | None = Field(
default=None,
description="Filter by resource name (partial match supported). Useful for finding specific resources like 'prod-db' or 'test-bucket'.",
),
tag_key: str | None = Field(
default=None,
description="Filter resources by tag key (e.g., 'Environment', 'CostCenter', 'Owner').",
),
tag_value: str | None = Field(
default=None,
description="Filter resources by tag value (e.g., 'production', 'staging', 'development').",
),
date_from: str | None = Field(
default=None,
description="Start date for range query in ISO 8601 format (YYYY-MM-DD, e.g., '2025-01-15'). Full date required. IMPORTANT: Maximum date range is 2 days. If only date_from is provided, date_to is automatically set to 2 days later.",
),
date_to: str | None = Field(
default=None,
description="End date for range query in ISO 8601 format (YYYY-MM-DD, e.g., '2025-01-15'). Full date required. If only date_to is provided, date_from is automatically set to 2 days earlier.",
),
search: str | None = Field(
default=None, description="Free-text search term across resource details"
),
page_size: int = Field(
default=50, description="Number of results to return per page (max 1000)"
),
page_number: int = Field(
default=1, description="Page number to retrieve (1-indexed)"
),
) -> dict[str, Any]:
"""List and filter all resources scanned by Prowler.
IMPORTANT: This tool returns LIGHTWEIGHT resource information. Use this for fast searching
and filtering across many resources. For complete configuration details, metadata, and finding
relationships, use prowler_app_get_resource on specific resources of interest.
This is the primary tool for browsing resources with rich filtering capabilities.
Returns current state by default (latest scan per provider). Specify dates to query
historical data (2-day maximum window).
Default behavior:
- Returns latest resources from most recent scans (no date parameters needed)
- Returns 50 results per page
- Sorted by service, region, and name for logical grouping
Date filtering:
- Without dates: queries resources from the most recent completed scan per provider (most efficient)
- With dates: queries historical resource state (2-day maximum range between date_from and date_to)
Each resource includes:
- Core identification: id (UUID for prowler_app_get_resource), uid, name
- Location context: region, service, type
- Security context: failed_findings_count (number of active security issues)
- Tags: tags associated with the resource
Useful Workflow:
1. Use this tool to search and filter resources by provider, region, service, tags, etc.
2. Use prowler_app_get_resource with the resource 'id' to get complete configuration and metadata
3. Use prowler_app_search_security_findings to find security issues for specific resources
4. Use prowler_app_get_finding_details to get details about the security issues for specific resources
"""
# Validate page_size parameter
self.api_client.validate_page_size(page_size)
# Determine endpoint based on date parameters
date_range = self.api_client.normalize_date_range(
date_from, date_to, max_days=2
)
if date_range is None:
# No dates provided - use latest resources endpoint
endpoint = "/api/v1/resources/latest"
params = {}
else:
# Dates provided - use historical resources endpoint
endpoint = "/api/v1/resources"
params = {
"filter[updated_at__gte]": date_range[0],
"filter[updated_at__lte]": date_range[1],
}
# Build filter parameters
if provider_type:
params["filter[provider_type__in]"] = provider_type
if provider_alias:
params["filter[provider_alias__icontains]"] = provider_alias
if provider_uid:
params["filter[provider_uid__icontains]"] = provider_uid
if region:
params["filter[region__in]"] = region
if service:
params["filter[service__in]"] = service
if resource_type:
params["filter[type__in]"] = resource_type
if resource_name:
params["filter[name__icontains]"] = resource_name
if tag_key:
params["filter[tag_key]"] = tag_key
if tag_value:
params["filter[tag_value]"] = tag_value
if search:
params["filter[search]"] = search
# Pagination
params["page[size]"] = page_size
params["page[number]"] = page_number
# Return only LLM-relevant fields
params["fields[resources]"] = (
"uid,name,region,service,type,failed_findings_count,tags"
)
params["sort"] = "service,region,name"
# Convert lists to comma-separated strings
clean_params = self.api_client.build_filter_params(params)
# Get API response and transform to simplified format
api_response = await self.api_client.get(endpoint, params=clean_params)
simplified_response = ResourcesListResponse.from_api_response(api_response)
return simplified_response.model_dump()
async def get_resource(
self,
resource_id: str = Field(
description="Prowler's internal UUID (v4) for the resource to retrieve, generated when the resource was discovered in the system. Use `prowler_app_list_resources` tool to find the right ID"
),
) -> dict[str, Any]:
"""Retrieve comprehensive details about a specific resource by its ID.
IMPORTANT: This tool provides COMPLETE resource details with all available information.
Use this after finding a specific resource via prowler_app_list_resources.
This tool provides ALL information that prowler_app_list_resources returns PLUS:
1. Configuration Details:
- metadata: Provider-specific configuration (tags, policies, encryption settings, network rules)
- partition: Provider-specific partition/region grouping (e.g., aws, aws-cn, aws-us-gov for AWS)
2. Temporal Tracking:
- inserted_at: When Prowler first discovered this resource
- updated_at: When resource configuration last changed
3. Security Relationships:
- finding_ids: Prowler's internal UUIDs (v4) of all security findings associated with this resource
- Use prowler_app_get_finding_details on these IDs to get remediation guidance
Useful Workflow:
1. Use prowler_app_list_resources to browse and filter across many resources
2. Use this tool to drill down into specific resources of interest
3. Use prowler_app_get_finding_details to get details about the security issues for specific resources
"""
params = {}
# Get API response and transform to detailed format
api_response = await self.api_client.get(
f"/api/v1/resources/{resource_id}", params=params
)
self.logger.info(f"API response: {api_response}")
detailed_resource = DetailedResource.from_api_response(
api_response.get("data", {})
)
return detailed_resource.model_dump()
async def get_resources_overview(
self,
provider_type: list[str] = Field(
default=[],
description="Filter by provider type. Multiple values allowed. If empty, all providers are returned. For valid values, please refer to Prowler Hub/Prowler Documentation that you can also find in form of tools in this MCP Server.",
),
provider_alias: str | None = Field(
default=None,
description="Filter by specific provider alias/name (partial match supported).",
),
provider_uid: str | None = Field(
default=None,
description="Filter by provider's native ID (e.g., AWS account ID, Azure subscription ID).",
),
date_from: str | None = Field(
default=None,
description="Start date for range query in ISO 8601 format (YYYY-MM-DD). Maximum 2-day range.",
),
date_to: str | None = Field(
default=None,
description="End date for range query in ISO 8601 format (YYYY-MM-DD).",
),
) -> dict[str, Any]:
"""Generate a markdown overview of your resources with statistics and insights.
IMPORTANT: This tool provides HIGH-LEVEL STATISTICS without returning individual resources.
Use this when you need a summary view before drilling into details.
The report includes:
- Total number of resources
- Available services across your providers
- Regions where resources are deployed
- Resource types present in your providers
Output format: Markdown-formatted report ready to present to users or include in documentation.
Use cases:
- Understanding infrastructure footprint
- Identifying resource concentration (which regions, services)
- Multi-provider deployment auditing
- Resource inventory reporting
- Tags planning (by provider, service, region)
"""
# Determine endpoint based on date parameters
date_range = self.api_client.normalize_date_range(
date_from, date_to, max_days=2
)
if date_range is None:
# No dates provided - use latest metadata endpoint
metadata_endpoint = "/api/v1/resources/metadata/latest"
list_endpoint = "/api/v1/resources/latest"
params = {}
else:
# Dates provided - use historical endpoints
metadata_endpoint = "/api/v1/resources/metadata"
list_endpoint = "/api/v1/resources"
params = {
"filter[updated_at__gte]": date_range[0],
"filter[updated_at__lte]": date_range[1],
}
# Build common filter parameters
if provider_type:
params["filter[provider_type__in]"] = provider_type
if provider_alias:
params["filter[provider_alias__icontains]"] = provider_alias
if provider_uid:
params["filter[provider_uid__icontains]"] = provider_uid
# Convert lists to comma-separated strings
clean_params = self.api_client.build_filter_params(params)
# Get metadata (services, regions, types)
metadata_params = clean_params.copy()
metadata_params["fields[resources-metadata]"] = "services,regions,types"
metadata_response = await self.api_client.get(
metadata_endpoint, params=metadata_params
)
metadata = ResourcesMetadataResponse.from_api_response(metadata_response)
# Get total count (using page_size=1 for efficiency)
count_params = clean_params.copy()
count_params["page[size]"] = 1
count_params["page[number]"] = 1
count_response = await self.api_client.get(list_endpoint, params=count_params)
total_resources = (
count_response.get("meta", {}).get("pagination", {}).get("count", 0)
)
# Build markdown report
report_lines = ["# Cloud Resources Overview", ""]
# Total resources
report_lines.append(f"**Total Resources**: {total_resources:,} resources")
report_lines.append("")
# Services
if metadata.services:
report_lines.append("## Services")
report_lines.append(f"**{len(metadata.services)}** unique services found")
report_lines.append("")
for i, service in enumerate(metadata.services, 1):
report_lines.append(f"{i}. {service}")
report_lines.append("")
# Regions
if metadata.regions:
report_lines.append("## Regions")
report_lines.append(f"**{len(metadata.regions)}** unique regions found")
report_lines.append("")
for i, region in enumerate(metadata.regions, 1):
report_lines.append(f"{i}. {region}")
report_lines.append("")
# Resource types
if metadata.types:
report_lines.append("## Resource Types")
report_lines.append(
f"**{len(metadata.types)}** unique resource types found"
)
report_lines.append("")
for i, rtype in enumerate(metadata.types, 1):
report_lines.append(f"{i}. {rtype}")
report_lines.append("")
report = "\n".join(report_lines)
return {"report": report}
@@ -1,330 +0,0 @@
"""Security Scans tools for Prowler App MCP Server.
This module provides tools for managing and monitoring Prowler security scans.
"""
from typing import Any, Literal
from prowler_mcp_server.prowler_app.models.scans import (
DetailedScan,
ScanCreationResult,
ScansListResponse,
ScheduleCreationResult,
)
from prowler_mcp_server.prowler_app.tools.base import BaseTool
from pydantic import Field
class ScansTools(BaseTool):
"""Tools for security scan operations.
Provides tools for:
- prowler_app_list_scans: Search and filter scans with rich filtering capabilities
- prowler_app_get_scan: Get comprehensive details about a specific scan
- prowler_app_trigger_scan: Trigger manual security scans for providers
- prowler_app_schedule_daily_scan: Schedule automated daily scans for continuous monitoring
- prowler_app_update_scan: Update scan names for better organization
"""
async def list_scans(
self,
provider_id: list[str] = Field(
default=[],
description="Filter by Prowler's internal UUID(s) (v4) for specific provider(s), generated when the provider was registered. Use `prowler_app_search_providers` tool to find provider IDs",
),
provider_type: list[str] = Field(
default=[],
description="Filter by cloud provider type. For all valid values, please refer to Prowler Hub/Prowler Documentation that you can also find in form of tools in this MCP Server",
),
provider_alias: str | None = Field(
default=None,
description="Filter by provider alias/friendly name. Partial match supported (case-insensitive)",
),
state: list[
Literal[
"available",
"scheduled",
"executing",
"completed",
"failed",
"cancelled",
]
] = Field(
default=[],
description="Filter by scan execution state.",
),
trigger: Literal["manual", "scheduled"] | None = Field(
default=None,
description="Filter by how the scan was initiated. Options: 'manual' (user-initiated via prowler_app_trigger_scan), 'scheduled' (automated via prowler_app_schedule_daily_scan)",
),
name: str | None = Field(
default=None,
description="Filter by scan name. Partial match supported (case-insensitive)",
),
page_size: int = Field(
default=50,
description="Number of results to return per page",
),
page_number: int = Field(
default=1,
description="Page number to retrieve (1-indexed)",
),
) -> dict[str, Any]:
"""List and filter security scans across all providers with rich filtering capabilities.
IMPORTANT: This tool returns LIGHTWEIGHT scan information. Use this for fast searching and filtering
across many scans. For complete scan details including progress, duration, and resource counts,
use prowler_app_get_scan on specific scans of interest.
Default behavior:
- Returns all scans
- Returns 50 scans per page
- Includes all scan states (available, scheduled, executing, completed, failed, cancelled)
Each scan includes:
- Core identification: id (UUID for prowler_app_get_scan), name
- Execution context: state, trigger (manual/scheduled)
- Temporal data: started_at, completed_at
- Provider relationship: provider_id
Workflow:
1. Use this tool to search and filter scans by provider, state, or date range
2. Use prowler_app_get_scan with the scan 'id' to get progress, duration, and resource counts
3. Use prowler_app_search_security_findings filtered by scan dates to analyze scan results
"""
# Validate pagination
self.api_client.validate_page_size(page_size)
# Build query parameters
params: dict[str, Any] = {
"page[size]": page_size,
"page[number]": page_number,
}
# Apply provider filters
if provider_id:
params["filter[provider__in]"] = provider_id
if provider_type:
params["filter[provider_type__in]"] = provider_type
if provider_alias:
params["filter[provider_alias__icontains]"] = provider_alias
# Apply scan filters
if state:
params["filter[state__in]"] = state
if trigger:
params["filter[trigger]"] = trigger
if name:
params["filter[name__icontains]"] = name
clean_params = self.api_client.build_filter_params(params)
api_response = await self.api_client.get("/api/v1/scans", params=clean_params)
simplified_response = ScansListResponse.from_api_response(api_response)
return simplified_response.model_dump()
async def get_scan(
self,
scan_id: str = Field(
description="Prowler's internal UUID (v4) for the scan to retrieve, generated when the scan was created (e.g., '123e4567-e89b-12d3-a456-426614174000'). Use `prowler_app_list_scans` tool to find scan IDs"
),
) -> dict[str, Any]:
"""Retrieve comprehensive details about a specific scan by its ID.
IMPORTANT: This tool returns COMPLETE scan details.
Use this after finding a specific scan via prowler_app_list_scans.
This tool provides ALL information that prowler_app_list_scans returns PLUS:
1. Execution Details:
- progress: Scan completion progress as percentage (0-100%)
- duration: Total scan duration in seconds from start to completion
- unique_resource_count: Number of unique cloud resources discovered during the scan
2. Temporal Metadata:
- inserted_at: When the scan was created in the database
- scheduled_at: When the scan was scheduled to run (for scheduled scans)
- next_scan_at: When the next scan will run (for recurring daily scans)
Useful for:
- Monitoring scan progress during execution (via progress field)
- Viewing scan results and metrics after completion
- Debugging failed scans with detailed state information
- Understanding scan scheduling patterns
Workflow:
1. Use prowler_app_list_scans to browse and filter scans
2. Use this tool with the scan 'id' to monitor progress or view detailed results
3. For completed scans, use prowler_app_search_security_findings filtered by date to analyze findings
"""
# Fetch scan with all fields
params = {
"fields[scans]": "name,trigger,state,progress,duration,unique_resource_count,started_at,completed_at,scheduled_at,next_scan_at,inserted_at"
}
api_response = await self.api_client.get(
f"/api/v1/scans/{scan_id}", params=params
)
detailed_scan = DetailedScan.from_api_response(api_response["data"])
return detailed_scan.model_dump()
async def trigger_scan(
self,
provider_id: str = Field(
description="Prowler's internal UUID (v4) for the provider to scan, generated when the provider was registered in the system (e.g., '4d0e2614-6385-4fa7-bf0b-c2e2f75c6877'). Use `prowler_app_search_providers` tool to find the provider ID"
),
name: str | None = Field(
default=None,
description="Optional human-friendly name for the scan. Use descriptive names to identify scan purpose or context, e.g., 'Weekly Production Security Audit', 'Pre-Deployment Validation', 'Compliance Check Q4 2025'",
),
) -> dict[str, Any]:
"""Trigger a manual security scan for a provider.
IMPORTANT: This tool returns immediately once the scan is created.
The scan will continue running in the background. Use `prowler_app_get_scan`
with the returned scan ID to monitor progress and check when it completes.
Example Useful Workflow:
1. Use `prowler_app_search_providers` to find the provider_id you want to scan
2. Use this tool to trigger the scan
3. Use `prowler_app_get_scan` with the returned scan 'id' to monitor progress
4. Once completed, use `prowler_app_search_security_findings` to analyze results
"""
try:
# Build request data
request_data: dict[str, Any] = {
"data": {
"type": "scans",
"attributes": {},
"relationships": {
"provider": {
"data": {
"type": "providers",
"id": provider_id,
},
},
},
},
}
if name:
request_data["data"]["attributes"]["name"] = name
# Create scan (returns Task)
self.logger.info(f"Creating scan for provider {provider_id}")
task_response = await self.api_client.post(
"/api/v1/scans", json_data=request_data
)
scan_id = (
task_response.get("data", {})
.get("attributes", {})
.get("task_args", {})
.get("scan_id", None)
)
if not scan_id:
raise Exception("No scan_id returned from scan creation")
self.logger.info(f"Scan created successfully: {scan_id}")
scan_response = await self.api_client.get(f"/api/v1/scans/{scan_id}")
scan_info = DetailedScan.from_api_response(scan_response["data"])
return ScanCreationResult(
scan=scan_info,
status="success",
message=f"Scan {scan_id} created successfully. The scan may take some time to complete. Use prowler_app_get_scan tool with this ID to monitor progress.",
).model_dump()
except Exception as e:
self.logger.error(f"Scan creation failed: {e}")
return ScanCreationResult(
scan=None,
status="failed",
message=f"Scan creation failed: {str(e)}",
).model_dump()
async def schedule_daily_scan(
self,
provider_id: str = Field(
description="Prowler's internal UUID (v4) for the provider to scan, generated when the provider was registered in the system (e.g., '4d0e2614-6385-4fa7-bf0b-c2e2f75c6877'). Use `prowler_app_search_providers` tool to find the provider ID"
),
) -> dict[str, Any]:
"""Schedule automated daily scans for a provider for continuous security monitoring.
Creates a recurring daily scan schedule that will automatically trigger
scans every 24 hours (starting from the moment the schedule is created).
The schedule persists until manually removed and will execute even when
you're not actively using the system.
IMPORTANT: This tool returns immediately once the daily schedule is created.
The schedule will be set up in the background. Use `prowler_app_list_scans`
filtered by provider_id and trigger='scheduled' to view scheduled scans.
IMPORTANT: This creates a PERSISTENT schedule. The provider will be scanned
automatically every 24 hours until the provider is deleted.
Example Useful Workflow:
1. Use `prowler_app_search_providers` to find the provider_id you want to monitor
2. Use this tool to create the daily schedule
3. Use `prowler_app_list_scans` filtered by provider_id to view scheduled and completed scans
4. Monitor findings over time with `prowler_app_search_security_findings`
"""
self.logger.info(f"Creating daily schedule for provider {provider_id}")
task_response = await self.api_client.post(
"/api/v1/schedules/daily",
json_data={
"data": {
"type": "daily-schedules",
"attributes": {
"provider_id": provider_id,
},
},
},
)
task_state = (
task_response.get("data", {}).get("attributes", {}).get("state", None)
)
if task_state == "available":
return_message = "Daily schedule created successfully. The schedule is being set up in the background. Use prowler_app_list_scans with provider_id filter to view scheduled scans."
else:
return_message = "Daily schedule creation failed. Please try again later."
return ScheduleCreationResult(
scheduled=(task_state == "available"),
message=return_message,
).model_dump()
async def update_scan(
self,
scan_id: str = Field(
description="Prowler's internal UUID (v4) for the scan to update, generated when the scan was created (e.g., '123e4567-e89b-12d3-a456-426614174000'). Use `prowler_app_list_scans` tool to find the scan ID if you only know the provider or scan name. Returns an error if the scan ID is invalid or not found."
),
name: str = Field(
description="New human-friendly name for the scan (3-100 characters). Use descriptive names to improve organization and tracking, e.g., 'Production Security Audit - Q4 2025', 'Post-Deployment Compliance Check'. IMPORTANT: Only the scan name can be updated - other attributes (state, progress, duration) are read-only and managed by the system."
),
) -> dict[str, Any]:
"""Update a scan's name for better organization and tracking.
IMPORTANT: Only the scan name can be updated. Other scan attributes
(state, progress, duration, etc.) are read-only and managed by the system.
Example Useful Workflow:
1. Use `prowler_app_list_scans` to find the scan you want to rename
2. Use this tool with the scan 'id' and new name
"""
api_response = await self.api_client.patch(
f"/api/v1/scans/{scan_id}",
json_data={
"data": {
"type": "scans",
"id": scan_id,
"attributes": {"name": name},
},
},
)
detailed_scan = DetailedScan.from_api_response(api_response["data"])
return detailed_scan.model_dump()
@@ -1,6 +1,5 @@
"""Shared API client utilities for Prowler App tools."""
import asyncio
from datetime import datetime, timedelta
from enum import Enum
from typing import Any, Dict
@@ -84,13 +83,7 @@ class ProwlerAPIClient(metaclass=SingletonMeta):
)
response.raise_for_status()
if not response.content:
return {
"success": True,
"status_code": response.status_code,
}
else:
return response.json()
return response.json()
except httpx.HTTPStatusError as e:
logger.error(f"HTTP error during {method.value} {path}: {e}")
error_detail: str = ""
@@ -187,68 +180,6 @@ class ProwlerAPIClient(metaclass=SingletonMeta):
"""
return await self._make_request(HTTPMethod.DELETE, path, params=params)
async def poll_task_until_complete(
self,
task_id: str,
timeout: int = 60,
poll_interval: float = 1.0,
) -> dict[str, any]:
"""Poll a task until it reaches a terminal state.
This method polls the task endpoint at regular intervals until the task
completes, fails, or times out. It's designed for async operations like
provider connection tests and deletions that return task IDs.
Args:
task_id: The UUID of the task to poll (UUID object or string)
timeout: Maximum time to wait in seconds (default: 60)
poll_interval: Time between polls in seconds (default: 1.0)
Returns:
The complete task response when terminal state is reached
Raises:
Exception: If task fails, is cancelled, or timeout is exceeded
"""
terminal_states = {"completed", "failed", "cancelled"}
start_time = asyncio.get_event_loop().time()
max_time = start_time + timeout
logger.info(
f"Polling task {task_id} (timeout: {timeout}s, interval: {poll_interval}s)"
)
while True:
# Check if we've exceeded the timeout
current_time = asyncio.get_event_loop().time()
if current_time >= max_time:
raise Exception(
f"Task {task_id} polling timed out after {timeout} seconds. "
f"The task may still be running. Try increasing the timeout or check task status manually."
)
# Fetch current task state
response = await self.get(f"/api/v1/tasks/{task_id}")
task_data = response.get("data", {})
task_attrs = task_data.get("attributes", {})
state = task_attrs.get("state")
logger.debug(f"Task {task_id} state: {state}")
# Check if we've reached a terminal state
if state in terminal_states:
if state == "completed":
logger.info(f"Task {task_id} completed successfully")
return response
elif state == "failed":
error_msg = task_attrs.get("error", "Unknown error")
raise Exception(f"Task {task_id} failed: {error_msg}")
elif state == "cancelled":
raise Exception(f"Task {task_id} was cancelled")
# Wait before next poll
await asyncio.sleep(poll_interval)
def _validate_date_format(self, date_str: str, param_name: str) -> datetime:
"""Validate date string format.
@@ -322,14 +253,6 @@ class ProwlerAPIClient(metaclass=SingletonMeta):
elif to_date and not from_date:
from_date = to_date - timedelta(days=max_days - 1)
# Validate that date_from is before or equal to date_to
if from_date > to_date:
raise ValueError(
f"Invalid date range: date_from must be before or equal to date_to. "
f"Got date_from='{from_date.date()}' and date_to='{to_date.date()}'. "
f"Please swap the dates or use the correct order."
)
# Validate range doesn't exceed max_days
delta: int = (to_date - from_date).days + 1
if delta > max_days:
Generated
+1 -8
View File
@@ -2923,8 +2923,6 @@ python-versions = "*"
groups = ["dev"]
files = [
{file = "jsonpath-ng-1.7.0.tar.gz", hash = "sha256:f6f5f7fd4e5ff79c785f1573b394043b39849fb2bb47bcead935d12b00beab3c"},
{file = "jsonpath_ng-1.7.0-py2-none-any.whl", hash = "sha256:898c93fc173f0c336784a3fa63d7434297544b7198124a68f9a3ef9597b0ae6e"},
{file = "jsonpath_ng-1.7.0-py3-none-any.whl", hash = "sha256:f3d7f9e848cba1b6da28c55b1c26ff915dc9e0b1ba7e752a53d6da8d5cbd00b6"},
]
[package.dependencies]
@@ -5515,7 +5513,6 @@ files = [
{file = "ruamel.yaml.clib-0.2.12-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f66efbc1caa63c088dead1c4170d148eabc9b80d95fb75b6c92ac0aad2437d76"},
{file = "ruamel.yaml.clib-0.2.12-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:22353049ba4181685023b25b5b51a574bce33e7f51c759371a7422dcae5402a6"},
{file = "ruamel.yaml.clib-0.2.12-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:932205970b9f9991b34f55136be327501903f7c66830e9760a8ffb15b07f05cd"},
{file = "ruamel.yaml.clib-0.2.12-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a52d48f4e7bf9005e8f0a89209bf9a73f7190ddf0489eee5eb51377385f59f2a"},
{file = "ruamel.yaml.clib-0.2.12-cp310-cp310-win32.whl", hash = "sha256:3eac5a91891ceb88138c113f9db04f3cebdae277f5d44eaa3651a4f573e6a5da"},
{file = "ruamel.yaml.clib-0.2.12-cp310-cp310-win_amd64.whl", hash = "sha256:ab007f2f5a87bd08ab1499bdf96f3d5c6ad4dcfa364884cb4549aa0154b13a28"},
{file = "ruamel.yaml.clib-0.2.12-cp311-cp311-macosx_13_0_arm64.whl", hash = "sha256:4a6679521a58256a90b0d89e03992c15144c5f3858f40d7c18886023d7943db6"},
@@ -5524,7 +5521,6 @@ files = [
{file = "ruamel.yaml.clib-0.2.12-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:811ea1594b8a0fb466172c384267a4e5e367298af6b228931f273b111f17ef52"},
{file = "ruamel.yaml.clib-0.2.12-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:cf12567a7b565cbf65d438dec6cfbe2917d3c1bdddfce84a9930b7d35ea59642"},
{file = "ruamel.yaml.clib-0.2.12-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:7dd5adc8b930b12c8fc5b99e2d535a09889941aa0d0bd06f4749e9a9397c71d2"},
{file = "ruamel.yaml.clib-0.2.12-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1492a6051dab8d912fc2adeef0e8c72216b24d57bd896ea607cb90bb0c4981d3"},
{file = "ruamel.yaml.clib-0.2.12-cp311-cp311-win32.whl", hash = "sha256:bd0a08f0bab19093c54e18a14a10b4322e1eacc5217056f3c063bd2f59853ce4"},
{file = "ruamel.yaml.clib-0.2.12-cp311-cp311-win_amd64.whl", hash = "sha256:a274fb2cb086c7a3dea4322ec27f4cb5cc4b6298adb583ab0e211a4682f241eb"},
{file = "ruamel.yaml.clib-0.2.12-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:20b0f8dc160ba83b6dcc0e256846e1a02d044e13f7ea74a3d1d56ede4e48c632"},
@@ -5533,7 +5529,6 @@ files = [
{file = "ruamel.yaml.clib-0.2.12-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:749c16fcc4a2b09f28843cda5a193e0283e47454b63ec4b81eaa2242f50e4ccd"},
{file = "ruamel.yaml.clib-0.2.12-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:bf165fef1f223beae7333275156ab2022cffe255dcc51c27f066b4370da81e31"},
{file = "ruamel.yaml.clib-0.2.12-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:32621c177bbf782ca5a18ba4d7af0f1082a3f6e517ac2a18b3974d4edf349680"},
{file = "ruamel.yaml.clib-0.2.12-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:b82a7c94a498853aa0b272fd5bc67f29008da798d4f93a2f9f289feb8426a58d"},
{file = "ruamel.yaml.clib-0.2.12-cp312-cp312-win32.whl", hash = "sha256:e8c4ebfcfd57177b572e2040777b8abc537cdef58a2120e830124946aa9b42c5"},
{file = "ruamel.yaml.clib-0.2.12-cp312-cp312-win_amd64.whl", hash = "sha256:0467c5965282c62203273b838ae77c0d29d7638c8a4e3a1c8bdd3602c10904e4"},
{file = "ruamel.yaml.clib-0.2.12-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:4c8c5d82f50bb53986a5e02d1b3092b03622c02c2eb78e29bec33fd9593bae1a"},
@@ -5542,7 +5537,6 @@ files = [
{file = "ruamel.yaml.clib-0.2.12-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:96777d473c05ee3e5e3c3e999f5d23c6f4ec5b0c38c098b3a5229085f74236c6"},
{file = "ruamel.yaml.clib-0.2.12-cp313-cp313-musllinux_1_1_i686.whl", hash = "sha256:3bc2a80e6420ca8b7d3590791e2dfc709c88ab9152c00eeb511c9875ce5778bf"},
{file = "ruamel.yaml.clib-0.2.12-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:e188d2699864c11c36cdfdada94d781fd5d6b0071cd9c427bceb08ad3d7c70e1"},
{file = "ruamel.yaml.clib-0.2.12-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4f6f3eac23941b32afccc23081e1f50612bdbe4e982012ef4f5797986828cd01"},
{file = "ruamel.yaml.clib-0.2.12-cp313-cp313-win32.whl", hash = "sha256:6442cb36270b3afb1b4951f060eccca1ce49f3d087ca1ca4563a6eb479cb3de6"},
{file = "ruamel.yaml.clib-0.2.12-cp313-cp313-win_amd64.whl", hash = "sha256:e5b8daf27af0b90da7bb903a876477a9e6d7270be6146906b276605997c7e9a3"},
{file = "ruamel.yaml.clib-0.2.12-cp39-cp39-macosx_12_0_arm64.whl", hash = "sha256:fc4b630cd3fa2cf7fce38afa91d7cfe844a9f75d7f0f36393fa98815e911d987"},
@@ -5551,7 +5545,6 @@ files = [
{file = "ruamel.yaml.clib-0.2.12-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e2f1c3765db32be59d18ab3953f43ab62a761327aafc1594a2a1fbe038b8b8a7"},
{file = "ruamel.yaml.clib-0.2.12-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:d85252669dc32f98ebcd5d36768f5d4faeaeaa2d655ac0473be490ecdae3c285"},
{file = "ruamel.yaml.clib-0.2.12-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:e143ada795c341b56de9418c58d028989093ee611aa27ffb9b7f609c00d813ed"},
{file = "ruamel.yaml.clib-0.2.12-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:2c59aa6170b990d8d2719323e628aaf36f3bfbc1c26279c0eeeb24d05d2d11c7"},
{file = "ruamel.yaml.clib-0.2.12-cp39-cp39-win32.whl", hash = "sha256:beffaed67936fbbeffd10966a4eb53c402fafd3d6833770516bf7314bc6ffa12"},
{file = "ruamel.yaml.clib-0.2.12-cp39-cp39-win_amd64.whl", hash = "sha256:040ae85536960525ea62868b642bdb0c2cc6021c9f9d507810c0c604e66f5a7b"},
{file = "ruamel.yaml.clib-0.2.12.tar.gz", hash = "sha256:6c8fbb13ec503f99a91901ab46e0b07ae7941cd527393187039aec586fdfd36f"},
@@ -6460,4 +6453,4 @@ files = [
[metadata]
lock-version = "2.1"
python-versions = ">3.9.1,<3.13"
content-hash = "1559a8799915bf0372eef07396e1dc40802911ef07ae92997cd260d9fe596ba3"
content-hash = "433468987cb3c4499d094d90e9f8cc9062a25ce115fde991a4e1b39edbfb7815"
+22 -32
View File
@@ -2,7 +2,7 @@
All notable changes to the **Prowler SDK** are documented in this file.
## [5.15.0] (Prowler v5.15.0)
## [v5.15.0] (Prowler UNRELEASED)
### Added
- `cloudstorage_uses_vpc_service_controls` check for GCP provider [(#9256)](https://github.com/prowler-cloud/prowler/pull/9256)
@@ -11,10 +11,6 @@ All notable changes to the **Prowler SDK** are documented in this file.
- `compute_instance_preemptible_vm_disabled` check for GCP provider [(#9342)](https://github.com/prowler-cloud/prowler/pull/9342)
- `compute_instance_automatic_restart_enabled` check for GCP provider [(#9271)](https://github.com/prowler-cloud/prowler/pull/9271)
- `compute_instance_deletion_protection_enabled` check for GCP provider [(#9358)](https://github.com/prowler-cloud/prowler/pull/9358)
- Update SOC2 - Azure with Processing Integrity requirements [(#9463)](https://github.com/prowler-cloud/prowler/pull/9463)
- Update SOC2 - GCP with Processing Integrity requirements [(#9464)](https://github.com/prowler-cloud/prowler/pull/9464)
- Update SOC2 - AWS with Processing Integrity requirements [(#9462)](https://github.com/prowler-cloud/prowler/pull/9462)
- RBI Cyber Security Framework compliance for Azure provider [(#8822)](https://github.com/prowler-cloud/prowler/pull/8822)
### Changed
- Update AWS Macie service metadata to new format [(#9265)](https://github.com/prowler-cloud/prowler/pull/9265)
@@ -25,22 +21,16 @@ All notable changes to the **Prowler SDK** are documented in this file.
- Update AWS Macie service metadata to new format [(#9265)](https://github.com/prowler-cloud/prowler/pull/9265)
- Update AWS Lightsail service metadata to new format [(#9264)](https://github.com/prowler-cloud/prowler/pull/9264)
### Fixed
- Fix duplicate requirement IDs in ISO 27001:2013 AWS compliance framework by adding unique letter suffixes
- Removed incorrect threat-detection category from checks metadata [(#9489)](https://github.com/prowler-cloud/prowler/pull/9489)
- GCP `cloudstorage_uses_vpc_service_controls` check to handle VPC Service Controls blocked API access [(#9478)](https://github.com/prowler-cloud/prowler/pull/9478)
---
## [5.14.2] (Prowler v5.14.2)
## [v5.14.2] (Prowler UNRELEASED)
### Fixed
- Custom check folder metadata validation [(#9335)](https://github.com/prowler-cloud/prowler/pull/9335)
- Pin `alibabacloud-gateway-oss-util` to version 0.0.3 to address missing dependency [(#9487)](https://github.com/prowler-cloud/prowler/pull/9487)
---
## [5.14.1] (Prowler v5.14.1)
## [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)
@@ -48,7 +38,7 @@ All notable changes to the **Prowler SDK** are documented in this file.
---
## [5.14.0] (Prowler v5.14.0)
## [v5.14.0] (Prowler v5.14.0)
### Added
- GitHub provider check `organization_default_repository_permission_strict` [(#8785)](https://github.com/prowler-cloud/prowler/pull/8785)
@@ -126,7 +116,7 @@ All notable changes to the **Prowler SDK** are documented in this file.
---
## [5.13.1] (Prowler v5.13.1)
## [v5.13.1] (Prowler v5.13.1)
### Fixed
- Add `resource_name` for checks under `logging` for the GCP provider [(#9023)](https://github.com/prowler-cloud/prowler/pull/9023)
@@ -142,7 +132,7 @@ All notable changes to the **Prowler SDK** are documented in this file.
---
## [5.13.0] (Prowler v5.13.0)
## [v5.13.0] (Prowler v5.13.0)
### Added
- Support for AdditionalURLs in outputs [(#8651)](https://github.com/prowler-cloud/prowler/pull/8651)
@@ -200,7 +190,7 @@ All notable changes to the **Prowler SDK** are documented in this file.
---
## [5.12.1] (Prowler v5.12.1)
## [v5.12.1] (Prowler v5.12.1)
### Fixed
- Replaced old check id with new ones for compliance files [(#8682)](https://github.com/prowler-cloud/prowler/pull/8682)
@@ -209,7 +199,7 @@ All notable changes to the **Prowler SDK** are documented in this file.
---
## [5.12.0] (Prowler v5.12.0)
## [v5.12.0] (Prowler v5.12.0)
### Added
- Add more fields for the Jira ticket and handle custom fields errors [(#8601)](https://github.com/prowler-cloud/prowler/pull/8601)
@@ -245,7 +235,7 @@ All notable changes to the **Prowler SDK** are documented in this file.
---
## [5.11.0] (Prowler v5.11.0)
## [v5.11.0] (Prowler v5.11.0)
### Added
- Certificate authentication for M365 provider [(#8404)](https://github.com/prowler-cloud/prowler/pull/8404)
@@ -276,7 +266,7 @@ All notable changes to the **Prowler SDK** are documented in this file.
---
## [5.10.2] (Prowler v5.10.2)
## [v5.10.2] (Prowler v5.10.2)
### Fixed
- Order requirements by ID in Prowler ThreatScore AWS compliance framework [(#8495)](https://github.com/prowler-cloud/prowler/pull/8495)
@@ -290,14 +280,14 @@ All notable changes to the **Prowler SDK** are documented in this file.
---
## [5.10.1] (Prowler v5.10.1)
## [v5.10.1] (Prowler v5.10.1)
### Fixed
- Remove invalid requirements from CIS 1.0 for GitHub provider [(#8472)](https://github.com/prowler-cloud/prowler/pull/8472)
---
## [5.10.0] (Prowler v5.10.0)
## [v5.10.0] (Prowler v5.10.0)
### Added
- `bedrock_api_key_no_administrative_privileges` check for AWS provider [(#8321)](https://github.com/prowler-cloud/prowler/pull/8321)
@@ -337,14 +327,14 @@ All notable changes to the **Prowler SDK** are documented in this file.
---
## [5.9.2] (Prowler v5.9.2)
## [v5.9.2] (Prowler v5.9.2)
### Fixed
- Use the correct resource name in `defender_domain_dkim_enabled` check [(#8334)](https://github.com/prowler-cloud/prowler/pull/8334)
---
## [5.9.0] (Prowler v5.9.0)
## [v5.9.0] (Prowler v5.9.0)
### Added
- `storage_smb_channel_encryption_with_secure_algorithm` check for Azure provider [(#8123)](https://github.com/prowler-cloud/prowler/pull/8123)
@@ -378,7 +368,7 @@ All notable changes to the **Prowler SDK** are documented in this file.
---
## [5.8.1] (Prowler v5.8.1)
## [v5.8.1] (Prowler 5.8.1)
### Fixed
- Detect wildcarded ARNs in sts:AssumeRole policy resources [(#8164)](https://github.com/prowler-cloud/prowler/pull/8164)
@@ -388,7 +378,7 @@ All notable changes to the **Prowler SDK** are documented in this file.
---
## [5.8.0] (Prowler v5.8.0)
## [v5.8.0] (Prowler v5.8.0)
### Added
@@ -450,7 +440,7 @@ All notable changes to the **Prowler SDK** are documented in this file.
---
## [5.7.5] (Prowler v5.7.5)
## [v5.7.5] (Prowler v5.7.5)
### Fixed
- Use unified timestamp for all requirements [(#8059)](https://github.com/prowler-cloud/prowler/pull/8059)
@@ -468,7 +458,7 @@ All notable changes to the **Prowler SDK** are documented in this file.
---
## [5.7.3] (Prowler v5.7.3)
## [v5.7.3] (Prowler v5.7.3)
### Fixed
- Automatically encrypt password in Microsoft365 provider [(#7784)](https://github.com/prowler-cloud/prowler/pull/7784)
@@ -476,7 +466,7 @@ All notable changes to the **Prowler SDK** are documented in this file.
---
## [5.7.2] (Prowler v5.7.2)
## [v5.7.2] (Prowler v5.7.2)
### Fixed
- `m365_powershell test_credentials` to use sanitized credentials [(#7761)](https://github.com/prowler-cloud/prowler/pull/7761)
@@ -488,7 +478,7 @@ All notable changes to the **Prowler SDK** are documented in this file.
---
## [5.7.0] (Prowler v5.7.0)
## [v5.7.0] (Prowler v5.7.0)
### Added
- Update the compliance list supported for each provider from docs [(#7694)](https://github.com/prowler-cloud/prowler/pull/7694)
@@ -516,7 +506,7 @@ All notable changes to the **Prowler SDK** are documented in this file.
---
## [5.6.0] (Prowler v5.6.0)
## [v5.6.0] (Prowler v5.6.0)
### Added
- SOC2 compliance framework to Azure [(#7489)](https://github.com/prowler-cloud/prowler/pull/7489)
@@ -585,7 +575,7 @@ All notable changes to the **Prowler SDK** are documented in this file.
---
## [5.5.1] (Prowler v5.5.1)
## [v5.5.1] (Prowler v5.5.1)
### Fixed
- Default name to contacts in Azure Defender [(#7483)](https://github.com/prowler-cloud/prowler/pull/7483)
File diff suppressed because it is too large Load Diff
-100
View File
@@ -547,106 +547,6 @@
"cloudwatch_log_group_retention_policy_specific_days_enabled",
"kinesis_stream_data_retention_period"
]
},
{
"Id": "pi_1_2",
"Name": "PI1.2 System inputs are measured and recorded completely, accurately, and timely to meet the entity's processing integrity commitments and system requirements",
"Description": "The entity implements policies and procedures over system inputs, including controls over completeness and accuracy, to result in products, services, and reporting to meet the entity's objectives. This includes defining accuracy targets, monitoring input quality, and creating detailed records of each input event.",
"Attributes": [
{
"ItemId": "pi_1_2",
"Section": "PI1.0 - Processing Integrity",
"Service": "aws",
"Type": "automated"
}
],
"Checks": [
"apigateway_restapi_logging_enabled",
"apigatewayv2_api_access_logging_enabled",
"elbv2_logging_enabled",
"elb_logging_enabled",
"wafv2_webacl_logging_enabled",
"waf_global_webacl_logging_enabled",
"cloudtrail_s3_dataevents_write_enabled",
"cloudfront_distributions_logging_enabled"
]
},
{
"Id": "pi_1_3",
"Name": "PI1.3 Data is processed completely, accurately, and timely as authorized to meet the entity's processing integrity commitments and system requirements",
"Description": "The entity implements controls to ensure data is processed completely, accurately, and timely. This includes defining processing specifications, identifying processing activities, detecting and correcting errors throughout processing, recording processing activities with accurate logs, and ensuring completeness and timeliness of processing.",
"Attributes": [
{
"ItemId": "pi_1_3",
"Section": "PI1.0 - Processing Integrity",
"Service": "aws",
"Type": "automated"
}
],
"Checks": [
"cloudtrail_multi_region_enabled",
"cloudtrail_log_file_validation_enabled",
"cloudtrail_cloudwatch_logging_enabled",
"cloudwatch_log_metric_filter_unauthorized_api_calls",
"cloudwatch_log_metric_filter_authentication_failures",
"cloudwatch_log_metric_filter_policy_changes",
"cloudwatch_log_metric_filter_root_usage",
"config_recorder_all_regions_enabled",
"rds_instance_integration_cloudwatch_logs",
"rds_cluster_integration_cloudwatch_logs",
"glue_etl_jobs_logging_enabled",
"stepfunctions_statemachine_logging_enabled"
]
},
{
"Id": "pi_1_4",
"Name": "PI1.4 System outputs are complete, accurate, distributed only to intended parties, and retained to meet the entity's processing integrity commitments and system requirements",
"Description": "The entity implements controls to ensure system outputs are delivered to authorized recipients in the correct format and protected against unauthorized access, modification, theft, destruction, or corruption. This includes output encryption, access controls, and audit trails for output delivery.",
"Attributes": [
{
"ItemId": "pi_1_4",
"Section": "PI1.0 - Processing Integrity",
"Service": "aws",
"Type": "automated"
}
],
"Checks": [
"s3_bucket_default_encryption",
"s3_bucket_kms_encryption",
"cloudwatch_log_group_kms_encryption_enabled",
"sns_topics_kms_encryption_at_rest_enabled",
"kinesis_stream_encrypted_at_rest",
"cloudfront_distributions_field_level_encryption_enabled",
"cloudwatch_log_group_not_publicly_accessible",
"cloudwatch_cross_account_sharing_disabled",
"glue_etl_jobs_cloudwatch_logs_encryption_enabled",
"glue_etl_jobs_amazon_s3_encryption_enabled"
]
},
{
"Id": "pi_1_5",
"Name": "PI1.5 Stored data is maintained complete, accurate, and protected from unauthorized modification to meet the entity's processing integrity commitments and system requirements",
"Description": "The entity implements controls to protect stored inputs, items in processing, and outputs from theft, destruction, corruption, or deterioration. This includes data encryption at rest, key management, backup and recovery procedures, access controls, and data integrity validation.",
"Attributes": [
{
"ItemId": "pi_1_5",
"Section": "PI1.0 - Processing Integrity",
"Service": "aws",
"Type": "automated"
}
],
"Checks": [
"s3_bucket_object_versioning",
"s3_bucket_object_lock",
"rds_instance_storage_encrypted",
"rds_cluster_storage_encrypted",
"dynamodb_tables_kms_cmk_encryption_enabled",
"ec2_ebs_volume_encryption",
"backup_plans_exist",
"backup_recovery_point_encrypted",
"backup_vaults_encrypted",
"kms_cmk_rotation_enabled"
]
}
]
}
@@ -1,248 +0,0 @@
{
"Framework": "RBI-Cyber-Security-Framework",
"Name": "Reserve Bank of India (RBI) Cyber Security Framework",
"Version": "",
"Provider": "Azure",
"Description": "The Reserve Bank had prescribed a set of baseline cyber security controls for primary (Urban) cooperative banks (UCBs) in October 2018. On further examination, it has been decided to prescribe a comprehensive cyber security framework for the UCBs, as a graded approach, based on their digital depth and interconnectedness with the payment systems landscape, digital products offered by them and assessment of cyber security risk. The framework would mandate implementation of progressively stronger security measures based on the nature, variety and scale of digital product offerings of banks.",
"Requirements": [
{
"Id": "annex_i_1_1",
"Name": "Annex I (1.1)",
"Description": "UCBs should maintain an up-to-date business IT Asset Inventory Register containing the following fields, as a minimum: a) Details of the IT Asset (viz., hardware/software/network devices, key personnel, services, etc.), b. Details of systems where customer data are stored, c. Associated business applications, if any, d. Criticality of the IT asset (For example, High/Medium/Low).",
"Attributes": [
{
"ItemId": "annex_i_1_1",
"Service": "vm"
}
],
"Checks": [
"vm_ensure_using_approved_images",
"vm_ensure_using_managed_disks",
"vm_trusted_launch_enabled",
"aks_cluster_rbac_enabled",
"aks_clusters_created_with_private_nodes",
"appinsights_ensure_is_configured",
"containerregistry_admin_user_disabled"
]
},
{
"Id": "annex_i_1_3",
"Name": "Annex I (1.3)",
"Description": "Appropriately manage and provide protection within and outside UCB/network, keeping in mind how the data/information is stored, transmitted, processed, accessed and put to use within/outside the UCB's network, and level of risk they are exposed to depending on the sensitivity of the data/information.",
"Attributes": [
{
"ItemId": "annex_i_1_3",
"Service": "azure"
}
],
"Checks": [
"keyvault_key_rotation_enabled",
"keyvault_access_only_through_private_endpoints",
"keyvault_private_endpoints",
"keyvault_rbac_enabled",
"app_function_not_publicly_accessible",
"app_ensure_http_is_redirected_to_https",
"app_minimum_tls_version_12",
"storage_blob_public_access_level_is_disabled",
"storage_secure_transfer_required_is_enabled",
"storage_ensure_encryption_with_customer_managed_keys",
"storage_ensure_minimum_tls_version_12",
"storage_default_network_access_rule_is_denied",
"storage_ensure_private_endpoints_in_storage_accounts",
"network_ssh_internet_access_restricted",
"sqlserver_unrestricted_inbound_access",
"sqlserver_tde_encryption_enabled",
"sqlserver_tde_encrypted_with_cmk",
"cosmosdb_account_use_private_endpoints",
"cosmosdb_account_firewall_use_selected_networks",
"mysql_flexible_server_ssl_connection_enabled",
"mysql_flexible_server_minimum_tls_version_12",
"postgresql_flexible_server_enforce_ssl_enabled",
"aks_clusters_public_access_disabled",
"containerregistry_not_publicly_accessible",
"containerregistry_uses_private_link",
"aisearch_service_not_publicly_accessible"
]
},
{
"Id": "annex_i_5_1",
"Name": "Annex I (5.1)",
"Description": "The firewall configurations should be set to the highest security level and evaluation of critical device (such as firewall, network switches, security devices, etc.) configurations should be done periodically.",
"Attributes": [
{
"ItemId": "annex_i_5_1",
"Service": "network"
}
],
"Checks": [
"network_rdp_internet_access_restricted",
"network_http_internet_access_restricted",
"network_udp_internet_access_restricted",
"network_ssh_internet_access_restricted",
"network_flow_log_captured_sent",
"network_flow_log_more_than_90_days",
"network_watcher_enabled",
"network_bastion_host_exists",
"aks_network_policy_enabled",
"storage_default_network_access_rule_is_denied"
]
},
{
"Id": "annex_i_6",
"Name": "Annex I (6)",
"Description": "Put in place systems and processes to identify, track, manage and monitor the status of patches to servers, operating system and application software running at the systems used by the UCB officials (end-users). Implement and update antivirus protection for all servers and applicable end points preferably through a centralised system.",
"Attributes": [
{
"ItemId": "annex_i_6",
"Service": "defender"
}
],
"Checks": [
"defender_ensure_system_updates_are_applied",
"defender_assessments_vm_endpoint_protection_installed",
"defender_ensure_defender_for_server_is_on",
"defender_ensure_defender_for_app_services_is_on",
"defender_ensure_defender_for_sql_servers_is_on",
"defender_ensure_defender_for_azure_sql_databases_is_on",
"defender_ensure_defender_for_storage_is_on",
"defender_ensure_defender_for_containers_is_on",
"defender_ensure_defender_for_keyvault_is_on",
"defender_ensure_defender_for_arm_is_on",
"defender_ensure_defender_for_dns_is_on",
"defender_ensure_defender_for_databases_is_on",
"defender_ensure_defender_for_cosmosdb_is_on",
"defender_container_images_scan_enabled",
"defender_container_images_resolved_vulnerabilities",
"defender_auto_provisioning_vulnerabilty_assessments_machines_on",
"vm_backup_enabled",
"app_ensure_java_version_is_latest",
"app_ensure_php_version_is_latest",
"app_ensure_python_version_is_latest"
]
},
{
"Id": "annex_i_7_1",
"Name": "Annex I (7.1)",
"Description": "Disallow administrative rights on end-user workstations/PCs/laptops and provide access rights on a 'need to know' and 'need to do' basis.",
"Attributes": [
{
"ItemId": "annex_i_7_1",
"Service": "iam"
}
],
"Checks": [
"iam_role_user_access_admin_restricted",
"iam_subscription_roles_owner_custom_not_created",
"iam_custom_role_has_permissions_to_administer_resource_locks",
"entra_global_admin_in_less_than_five_users",
"entra_policy_ensure_default_user_cannot_create_apps",
"entra_policy_ensure_default_user_cannot_create_tenants",
"entra_policy_default_users_cannot_create_security_groups",
"entra_policy_guest_invite_only_for_admin_roles",
"entra_policy_guest_users_access_restrictions",
"app_function_identity_without_admin_privileges"
]
},
{
"Id": "annex_i_7_2",
"Name": "Annex I (7.2)",
"Description": "Passwords should be set as complex and lengthy and users should not use same passwords for all the applications/systems/devices.",
"Attributes": [
{
"ItemId": "annex_i_7_2",
"Service": "entra"
}
],
"Checks": [
"entra_non_privileged_user_has_mfa",
"entra_privileged_user_has_mfa",
"entra_policy_user_consent_for_verified_apps",
"entra_policy_restricts_user_consent_for_apps",
"entra_user_with_vm_access_has_mfa",
"entra_security_defaults_enabled",
"entra_conditional_access_policy_require_mfa_for_management_api",
"entra_trusted_named_locations_exists",
"sqlserver_azuread_administrator_enabled",
"postgresql_flexible_server_entra_id_authentication_enabled",
"cosmosdb_account_use_aad_and_rbac"
]
},
{
"Id": "annex_i_7_3",
"Name": "Annex I (7.3)",
"Description": "Remote Desktop Protocol (RDP) which allows others to access the computer remotely over a network or over the internet should be always disabled and should be enabled only with the approval of the authorised officer of the UCB. Logs for such remote access shall be enabled and monitored for suspicious activities.",
"Attributes": [
{
"ItemId": "annex_i_7_3",
"Service": "network"
}
],
"Checks": [
"network_rdp_internet_access_restricted",
"vm_jit_access_enabled",
"network_bastion_host_exists",
"vm_linux_enforce_ssh_authentication"
]
},
{
"Id": "annex_i_7_4",
"Name": "Annex I (7.4)",
"Description": "Implement appropriate (e.g. centralised) systems and controls to allow, manage, log and monitor privileged/super user/administrative access to critical systems (servers/databases, applications, network devices etc.)",
"Attributes": [
{
"ItemId": "annex_i_7_4",
"Service": "monitor"
}
],
"Checks": [
"monitor_alert_create_update_nsg",
"monitor_alert_delete_nsg",
"monitor_diagnostic_setting_with_appropriate_categories",
"monitor_diagnostic_settings_exists",
"monitor_alert_create_policy_assignment",
"monitor_alert_delete_policy_assignment",
"monitor_alert_create_update_security_solution",
"monitor_alert_delete_security_solution",
"monitor_alert_create_update_sqlserver_fr",
"monitor_alert_delete_sqlserver_fr",
"monitor_alert_create_update_public_ip_address_rule",
"monitor_alert_delete_public_ip_address_rule",
"monitor_alert_service_health_exists",
"monitor_storage_account_with_activity_logs_cmk_encrypted",
"monitor_storage_account_with_activity_logs_is_private",
"keyvault_logging_enabled",
"sqlserver_auditing_enabled",
"sqlserver_auditing_retention_90_days",
"app_http_logs_enabled",
"app_function_application_insights_enabled",
"defender_additional_email_configured_with_a_security_contact",
"defender_ensure_notify_alerts_severity_is_high",
"defender_ensure_notify_emails_to_owners",
"defender_ensure_mcas_is_enabled",
"defender_ensure_wdatp_is_enabled"
]
},
{
"Id": "annex_i_12",
"Name": "Annex I (12)",
"Description": "Take periodic back up of the important data and store this data 'off line' (i.e., transferring important files to a storage device that can be detached from a computer/system after copying all the files).",
"Attributes": [
{
"ItemId": "annex_i_12",
"Service": "azure"
}
],
"Checks": [
"vm_backup_enabled",
"vm_sufficient_daily_backup_retention_period",
"storage_ensure_file_shares_soft_delete_is_enabled",
"storage_blob_versioning_is_enabled",
"storage_ensure_soft_delete_is_enabled",
"storage_geo_redundant_enabled",
"keyvault_recoverable",
"sqlserver_vulnerability_assessment_enabled",
"sqlserver_va_periodic_recurring_scans_enabled"
]
}
]
}
+1 -87
View File
@@ -619,92 +619,6 @@
"sqlserver_auditing_retention_90_days",
"storage_ensure_soft_delete_is_enabled"
]
},
{
"Id": "pi_1_2",
"Name": "PI1.2 System inputs are measured and recorded completely, accurately, and timely to meet the entity's processing integrity commitments and system requirements",
"Description": "The entity implements policies and procedures over system inputs, including controls over completeness and accuracy, to result in products, services, and reporting to meet the entity's objectives. This includes defining accuracy targets, monitoring input quality, and creating detailed records of each input event.",
"Attributes": [
{
"ItemId": "pi_1_2",
"Section": "PI1.0 - Processing Integrity",
"Service": "azure",
"Type": "automated"
}
],
"Checks": [
"app_http_logs_enabled",
"network_flow_log_captured_sent",
"keyvault_logging_enabled",
"monitor_diagnostic_settings_exists",
"sqlserver_auditing_enabled"
]
},
{
"Id": "pi_1_3",
"Name": "PI1.3 Data is processed completely, accurately, and timely as authorized to meet the entity's processing integrity commitments and system requirements",
"Description": "The entity implements controls to ensure data is processed completely, accurately, and timely. This includes defining processing specifications, identifying processing activities, detecting and correcting errors throughout processing, recording processing activities with accurate logs, and ensuring completeness and timeliness of processing.",
"Attributes": [
{
"ItemId": "pi_1_3",
"Section": "PI1.0 - Processing Integrity",
"Service": "azure",
"Type": "automated"
}
],
"Checks": [
"monitor_diagnostic_setting_with_appropriate_categories",
"monitor_diagnostic_settings_exists",
"defender_auto_provisioning_log_analytics_agent_vms_on",
"mysql_flexible_server_audit_log_enabled",
"postgresql_flexible_server_log_checkpoints_on",
"postgresql_flexible_server_log_connections_on",
"postgresql_flexible_server_log_disconnections_on",
"network_flow_log_more_than_90_days"
]
},
{
"Id": "pi_1_4",
"Name": "PI1.4 System outputs are complete, accurate, distributed only to intended parties, and retained to meet the entity's processing integrity commitments and system requirements",
"Description": "The entity implements controls to ensure system outputs are delivered to authorized recipients in the correct format and protected against unauthorized access, modification, theft, destruction, or corruption. This includes output encryption, access controls, and audit trails for output delivery.",
"Attributes": [
{
"ItemId": "pi_1_4",
"Section": "PI1.0 - Processing Integrity",
"Service": "azure",
"Type": "automated"
}
],
"Checks": [
"storage_ensure_encryption_with_customer_managed_keys",
"storage_infrastructure_encryption_is_enabled",
"monitor_storage_account_with_activity_logs_cmk_encrypted",
"monitor_storage_account_with_activity_logs_is_private",
"sqlserver_tde_encryption_enabled",
"sqlserver_tde_encrypted_with_cmk"
]
},
{
"Id": "pi_1_5",
"Name": "PI1.5 Stored data is maintained complete, accurate, and protected from unauthorized modification to meet the entity's processing integrity commitments and system requirements",
"Description": "The entity implements controls to protect stored inputs, items in processing, and outputs from theft, destruction, corruption, or deterioration. This includes data encryption at rest, key management, backup and recovery procedures, access controls, and data integrity validation.",
"Attributes": [
{
"ItemId": "pi_1_5",
"Section": "PI1.0 - Processing Integrity",
"Service": "azure",
"Type": "automated"
}
],
"Checks": [
"storage_ensure_encryption_with_customer_managed_keys",
"storage_infrastructure_encryption_is_enabled",
"storage_ensure_soft_delete_is_enabled",
"vm_ensure_attached_disks_encrypted_with_cmk",
"vm_ensure_unattached_disks_encrypted_with_cmk",
"keyvault_key_rotation_enabled",
"keyvault_recoverable"
]
}
]
}
}
+1 -82
View File
@@ -492,87 +492,6 @@
"Checks": [
"cloudstorage_bucket_log_retention_policy_lock"
]
},
{
"Id": "pi_1_2",
"Name": "PI1.2 System inputs are measured and recorded completely, accurately, and timely to meet the entity's processing integrity commitments and system requirements",
"Description": "The entity implements policies and procedures over system inputs, including controls over completeness and accuracy, to result in products, services, and reporting to meet the entity's objectives. This includes defining accuracy targets, monitoring input quality, and creating detailed records of each input event.",
"Attributes": [
{
"ItemId": "pi_1_2",
"Section": "PI1.0 - Processing Integrity",
"Service": "gcp",
"Type": "automated"
}
],
"Checks": [
"compute_loadbalancer_logging_enabled",
"compute_subnet_flow_logs_enabled",
"logging_sink_created",
"iam_audit_logs_enabled"
]
},
{
"Id": "pi_1_3",
"Name": "PI1.3 Data is processed completely, accurately, and timely as authorized to meet the entity's processing integrity commitments and system requirements",
"Description": "The entity implements controls to ensure data is processed completely, accurately, and timely. This includes defining processing specifications, identifying processing activities, detecting and correcting errors throughout processing, recording processing activities with accurate logs, and ensuring completeness and timeliness of processing.",
"Attributes": [
{
"ItemId": "pi_1_3",
"Section": "PI1.0 - Processing Integrity",
"Service": "gcp",
"Type": "automated"
}
],
"Checks": [
"logging_log_metric_filter_and_alert_for_audit_configuration_changes_enabled",
"logging_log_metric_filter_and_alert_for_project_ownership_changes_enabled",
"logging_log_metric_filter_and_alert_for_sql_instance_configuration_changes_enabled",
"cloudsql_instance_postgres_log_connections_flag",
"cloudsql_instance_postgres_log_disconnections_flag",
"cloudsql_instance_postgres_log_statement_flag",
"iam_audit_logs_enabled"
]
},
{
"Id": "pi_1_4",
"Name": "PI1.4 System outputs are complete, accurate, distributed only to intended parties, and retained to meet the entity's processing integrity commitments and system requirements",
"Description": "The entity implements controls to ensure system outputs are delivered to authorized recipients in the correct format and protected against unauthorized access, modification, theft, destruction, or corruption. This includes output encryption, access controls, and audit trails for output delivery.",
"Attributes": [
{
"ItemId": "pi_1_4",
"Section": "PI1.0 - Processing Integrity",
"Service": "gcp",
"Type": "automated"
}
],
"Checks": [
"cloudstorage_bucket_uniform_bucket_level_access",
"bigquery_dataset_cmk_encryption",
"bigquery_table_cmk_encryption",
"compute_instance_confidential_computing_enabled",
"pubsub_topic_encryption_with_cmk"
]
},
{
"Id": "pi_1_5",
"Name": "PI1.5 Stored data is maintained complete, accurate, and protected from unauthorized modification to meet the entity's processing integrity commitments and system requirements",
"Description": "The entity implements controls to protect stored inputs, items in processing, and outputs from theft, destruction, corruption, or deterioration. This includes data encryption at rest, key management, backup and recovery procedures, access controls, and data integrity validation.",
"Attributes": [
{
"ItemId": "pi_1_5",
"Section": "PI1.0 - Processing Integrity",
"Service": "gcp",
"Type": "automated"
}
],
"Checks": [
"cloudstorage_bucket_log_retention_policy_lock",
"cloudsql_instance_automated_backups",
"compute_instance_encryption_with_csek_enabled",
"kms_key_rotation_enabled",
"dataproc_encrypted_with_cmks_disabled"
]
}
]
}
}
@@ -29,7 +29,9 @@
"Url": "https://hub.prowler.com/check/apigateway_restapi_waf_acl_attached"
}
},
"Categories": [],
"Categories": [
"threat-detection"
],
"DependsOn": [],
"RelatedTo": [],
"Notes": "",
@@ -33,7 +33,7 @@
}
},
"Categories": [
"forensics-ready"
"threat-detection"
],
"DependsOn": [],
"RelatedTo": [],
@@ -34,7 +34,8 @@
}
},
"Categories": [
"logging"
"logging",
"threat-detection"
],
"DependsOn": [],
"RelatedTo": [],
@@ -35,7 +35,8 @@
}
},
"Categories": [
"logging"
"logging",
"threat-detection"
],
"DependsOn": [],
"RelatedTo": [],
@@ -32,7 +32,8 @@
}
},
"Categories": [
"logging"
"logging",
"threat-detection"
],
"DependsOn": [],
"RelatedTo": [],
@@ -29,7 +29,8 @@
}
},
"Categories": [
"logging"
"logging",
"threat-detection"
],
"DependsOn": [],
"RelatedTo": [],
@@ -36,7 +36,8 @@
}
},
"Categories": [
"logging"
"logging",
"threat-detection"
],
"DependsOn": [],
"RelatedTo": [],
@@ -34,7 +34,8 @@
}
},
"Categories": [
"logging"
"logging",
"threat-detection"
],
"DependsOn": [],
"RelatedTo": [],
@@ -32,7 +32,8 @@
}
},
"Categories": [
"logging"
"logging",
"threat-detection"
],
"DependsOn": [],
"RelatedTo": [],
@@ -32,7 +32,8 @@
}
},
"Categories": [
"logging"
"logging",
"threat-detection"
],
"DependsOn": [],
"RelatedTo": [],
@@ -38,7 +38,8 @@
}
},
"Categories": [
"logging"
"logging",
"threat-detection"
],
"DependsOn": [],
"RelatedTo": [],
@@ -33,7 +33,8 @@
}
},
"Categories": [
"logging"
"logging",
"threat-detection"
],
"DependsOn": [],
"RelatedTo": [],
@@ -37,7 +37,8 @@
}
},
"Categories": [
"logging"
"logging",
"threat-detection"
],
"DependsOn": [],
"RelatedTo": [],
@@ -36,6 +36,7 @@
}
},
"Categories": [
"threat-detection",
"logging"
],
"DependsOn": [],
@@ -32,7 +32,8 @@
}
},
"Categories": [
"trust-boundaries"
"trust-boundaries",
"threat-detection"
],
"DependsOn": [],
"RelatedTo": [],
@@ -1,6 +1,5 @@
from typing import Optional
from googleapiclient.errors import HttpError
from pydantic.v1 import BaseModel
from prowler.lib.logger import logger
@@ -13,7 +12,6 @@ class CloudStorage(GCPService):
def __init__(self, provider: GcpProvider):
super().__init__("storage", provider)
self.buckets = []
self.vpc_service_controls_protected_projects = set()
self._get_buckets()
def _get_buckets(self):
@@ -95,17 +93,6 @@ class CloudStorage(GCPService):
request = self.client.buckets().list_next(
previous_request=request, previous_response=response
)
except HttpError as http_error:
# Check if the error is due to VPC Service Controls blocking the API
if "vpcServiceControlsUniqueIdentifier" in str(http_error):
self.vpc_service_controls_protected_projects.add(project_id)
logger.warning(
f"Project {project_id} is protected by VPC Service Controls for Cloud Storage API."
)
else:
logger.error(
f"{http_error.__class__.__name__}[{http_error.__traceback__.tb_lineno}]: {http_error}"
)
except Exception as error:
logger.error(
f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
@@ -5,22 +5,14 @@ from prowler.providers.gcp.services.accesscontextmanager.accesscontextmanager_cl
from prowler.providers.gcp.services.cloudresourcemanager.cloudresourcemanager_client import (
cloudresourcemanager_client,
)
from prowler.providers.gcp.services.cloudstorage.cloudstorage_client import (
cloudstorage_client,
)
class cloudstorage_uses_vpc_service_controls(Check):
"""
Ensure Cloud Storage is protected by VPC Service Controls at project level.
Reports PASS if:
- A project is in a VPC Service Controls perimeter with storage.googleapis.com
as a restricted service, OR
- The Cloud Storage API access is blocked by VPC Service Controls
(verified by vpcServiceControlsUniqueIdentifier in the error response)
Otherwise reports FAIL.
Reports PASS if a project is in a VPC Service Controls perimeter
with storage.googleapis.com as a restricted service, otherwise FAIL.
"""
def execute(self) -> list[Check_Report_GCP]:
@@ -55,12 +47,6 @@ class cloudstorage_uses_vpc_service_controls(Check):
if project_resource_id in protected_projects:
report.status = "PASS"
report.status_extended = f"Project {project.id} has VPC Service Controls enabled for Cloud Storage in perimeter {protected_projects[project_resource_id]}."
elif (
project.id
in cloudstorage_client.vpc_service_controls_protected_projects
):
report.status = "PASS"
report.status_extended = f"Project {project.id} has VPC Service Controls enabled for Cloud Storage in undetermined perimeter (verified by API access restriction)."
findings.append(report)
-1
View File
@@ -78,7 +78,6 @@ dependencies = [
"alibabacloud_ecs20140526==7.2.5",
"alibabacloud_sas20181203==6.1.0",
"alibabacloud_oss20190517==1.0.6",
"alibabacloud-gateway-oss-util==0.0.3",
"alibabacloud_actiontrail20200706==2.4.1",
"alibabacloud_cs20151215==6.1.0",
"alibabacloud-rds20140815==12.0.0",
@@ -1,6 +1,4 @@
from unittest.mock import MagicMock, patch
from googleapiclient.errors import HttpError
from unittest.mock import patch
from prowler.providers.gcp.services.cloudstorage.cloudstorage_service import (
CloudStorage,
@@ -58,36 +56,3 @@ class TestCloudStorageService:
assert not cloudstorage_client.buckets[1].public
assert cloudstorage_client.buckets[1].retention_policy is None
assert cloudstorage_client.buckets[1].project_id == GCP_PROJECT_ID
def test_vpc_service_controls_blocked(self):
with (
patch(
"prowler.providers.gcp.lib.service.service.GCPService.__is_api_active__",
new=mock_is_api_active,
),
patch(
"prowler.providers.gcp.lib.service.service.GCPService.__generate_client__",
) as mock_client,
):
mock_resp = MagicMock()
mock_resp.status = 403
mock_resp.reason = "Forbidden"
vpc_error = HttpError(
resp=mock_resp,
content=b'{"error": {"message": "Request is prohibited by organization\'s policy. vpcServiceControlsUniqueIdentifier: 12345"}}',
)
mock_buckets = MagicMock()
mock_buckets.list.return_value.execute.side_effect = vpc_error
mock_client.return_value.buckets.return_value = mock_buckets
cloudstorage_client = CloudStorage(
set_mocked_gcp_provider(project_ids=[GCP_PROJECT_ID])
)
assert (
GCP_PROJECT_ID
in cloudstorage_client.vpc_service_controls_protected_projects
)
assert len(cloudstorage_client.buckets) == 0
@@ -315,70 +315,3 @@ class TestCloudStorageUsesVPCServiceControls:
result = check.execute()
assert len(result) == 0
def test_project_protected_by_vpc_sc_api_blocked(self):
cloudresourcemanager_client = mock.MagicMock()
accesscontextmanager_client = mock.MagicMock()
cloudstorage_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_uses_vpc_service_controls.cloudstorage_uses_vpc_service_controls.cloudresourcemanager_client",
new=cloudresourcemanager_client,
),
mock.patch(
"prowler.providers.gcp.services.cloudstorage.cloudstorage_uses_vpc_service_controls.cloudstorage_uses_vpc_service_controls.accesscontextmanager_client",
new=accesscontextmanager_client,
),
mock.patch(
"prowler.providers.gcp.services.cloudstorage.cloudstorage_uses_vpc_service_controls.cloudstorage_uses_vpc_service_controls.cloudstorage_client",
new=cloudstorage_client,
),
):
from prowler.providers.gcp.services.cloudresourcemanager.cloudresourcemanager_service import (
Project,
)
from prowler.providers.gcp.services.cloudstorage.cloudstorage_uses_vpc_service_controls.cloudstorage_uses_vpc_service_controls import (
cloudstorage_uses_vpc_service_controls,
)
project1 = Project(
id=GCP_PROJECT_ID, number="123456789012", audit_logging=True
)
cloudresourcemanager_client.project_ids = [GCP_PROJECT_ID]
cloudresourcemanager_client.cloud_resource_manager_projects = [project1]
cloudresourcemanager_client.projects = {
GCP_PROJECT_ID: GCPProject(
id=GCP_PROJECT_ID,
number="123456789012",
name="test-project",
labels={},
lifecycle_state="ACTIVE",
)
}
cloudresourcemanager_client.region = GCP_US_CENTER1_LOCATION
# No service perimeters configured, but API access is blocked by VPC SC
accesscontextmanager_client.service_perimeters = []
cloudstorage_client.vpc_service_controls_protected_projects = {
GCP_PROJECT_ID
}
check = cloudstorage_uses_vpc_service_controls()
result = check.execute()
assert len(result) == 1
assert result[0].status == "PASS"
assert (
result[0].status_extended
== f"Project {GCP_PROJECT_ID} has VPC Service Controls enabled for Cloud Storage in undetermined perimeter (verified by API access restriction)."
)
assert result[0].resource_id == GCP_PROJECT_ID
assert result[0].resource_name == "test-project"
assert result[0].location == GCP_US_CENTER1_LOCATION
assert result[0].project_id == GCP_PROJECT_ID
+1 -1
View File
@@ -62,7 +62,7 @@ You are a code reviewer for the Prowler UI project. Analyze the full file conten
**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. Exception: `var()` is allowed when passing colors to chart/graph components that require CSS color strings (not Tailwind classes) for their APIs.
3. Tailwind: NO `var()` or hex colors in className → Use Tailwind utilities and semantic color classes. Exception: `var()` is allowed when passing colors to chart/graph components that require CSS color strings (not Tailwind classes) for their APIs
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.
+6 -4
View File
@@ -2,13 +2,12 @@
All notable changes to the **Prowler UI** are documented in this file.
## [1.15.0] (Prowler v5.15.0)
## [1.15.0] (Prowler Unreleased)
### 🚀 Added
- Risk Plot component with interactive legend and severity navigation to Overview page [(#9469)](https://github.com/prowler-cloud/prowler/pull/9469)
- Navigation progress bar for page transitions using Next.js `onRouterTransitionStart` [(#9465)](https://github.com/prowler-cloud/prowler/pull/9465)
- Findings Severity Over Time chart component to Overview page [(#9405)](https://github.com/prowler-cloud/prowler/pull/9405)
- Finding Severity Over Time chart component to Overview page [(#9405)](https://github.com/prowler-cloud/prowler/pull/9405)
- Attack Surface component to Overview page [(#9412)](https://github.com/prowler-cloud/prowler/pull/9412)
### 🔄 Changed
@@ -22,8 +21,11 @@ All notable changes to the **Prowler UI** are documented in this file.
- MongoDB Atlas provider support [(#9253)](https://github.com/prowler-cloud/prowler/pull/9253)
- Lighthouse AI support for Amazon Bedrock API key [(#9343)](https://github.com/prowler-cloud/prowler/pull/9343)
### 🐞 Fixed
---
## [1.14.3] (Prowler Unreleased)
### 🐞 Fixed
- Show top failed requirements in compliance specific view for compliance without sections [(#9471)](https://github.com/prowler-cloud/prowler/pull/9471)
---
@@ -1,9 +1,5 @@
"use server";
import {
getDateFromForTimeRange,
type TimeRange,
} from "@/app/(prowler)/_new-overview/severity-over-time/_constants/time-range.constants";
import { apiBaseUrl, getAuthHeaders } from "@/lib";
import { handleApiResponse } from "@/lib/server-actions-helper";
@@ -13,6 +9,20 @@ import {
FindingsSeverityOverTimeResponse,
} from "./types";
const TIME_RANGE_VALUES = {
FIVE_DAYS: "5D",
ONE_WEEK: "1W",
ONE_MONTH: "1M",
} as const;
type TimeRange = (typeof TIME_RANGE_VALUES)[keyof typeof TIME_RANGE_VALUES];
const TIME_RANGE_DAYS: Record<TimeRange, number> = {
"5D": 5,
"1W": 7,
"1M": 30,
};
export type SeverityTrendsResult =
| { status: "success"; data: AdaptedSeverityTrendsResponse }
| { status: "empty" }
@@ -66,9 +76,21 @@ export const getSeverityTrendsByTimeRange = async ({
timeRange: TimeRange;
filters?: Record<string, string | string[] | undefined>;
}): Promise<SeverityTrendsResult> => {
const days = TIME_RANGE_DAYS[timeRange];
if (!days) {
console.error("Invalid time range provided");
return { status: "error" };
}
const endDate = new Date();
const startDate = new Date(endDate.getTime() - days * 24 * 60 * 60 * 1000);
const dateFrom = startDate.toISOString().split("T")[0];
const dateFilters = {
...filters,
"filter[date_from]": getDateFromForTimeRange(timeRange),
date_from: dateFrom,
};
return getFindingsSeverityTrends({ filters: dateFilters });
@@ -267,10 +267,6 @@ export function RiskPlotClient({ data }: RiskPlotClientProps) {
<h3 className="text-text-neutral-primary text-lg font-semibold">
Risk Plot
</h3>
<p className="text-text-neutral-tertiary mt-1 text-xs">
Threat Score is severity-weighted, not quantity-based. Higher
severity findings have greater impact on the score.
</p>
</div>
<div className="relative min-h-[400px] w-full flex-1">
@@ -302,9 +298,9 @@ export function RiskPlotClient({ data }: RiskPlotClientProps) {
<YAxis
type="number"
dataKey="y"
name="Fail Findings"
name="Failed Findings"
label={{
value: "Fail Findings",
value: "Failed Findings",
angle: -90,
position: "left",
offset: 10,
@@ -342,7 +338,7 @@ export function RiskPlotClient({ data }: RiskPlotClientProps) {
{/* Interactive Legend - below chart */}
<div className="mt-4 flex flex-col items-start gap-2">
<p className="text-text-neutral-tertiary pl-2 text-xs">
Click to filter by provider
Click to filter by provider.
</p>
<ChartLegend
items={providers.map((p) => ({
@@ -367,7 +363,7 @@ export function RiskPlotClient({ data }: RiskPlotClientProps) {
{selectedPoint.name}
</h4>
<p className="text-text-neutral-tertiary text-xs">
Threat Score: {selectedPoint.x}% | Fail Findings:{" "}
Threat Score: {selectedPoint.x}% | Failed Findings:{" "}
{selectedPoint.y}
</p>
</div>
@@ -7,12 +7,12 @@ import { getSeverityTrendsByTimeRange } from "@/actions/overview/severity-trends
import { LineChart } from "@/components/graphs/line-chart";
import { LineConfig, LineDataPoint } from "@/components/graphs/types";
import {
MUTED_COLOR,
SEVERITY_LEVELS,
SEVERITY_LINE_CONFIGS,
SeverityLevel,
} from "@/types/severities";
import { DEFAULT_TIME_RANGE } from "../_constants/time-range.constants";
import { type TimeRange, TimeRangeSelector } from "./time-range-selector";
interface FindingSeverityOverTimeProps {
@@ -24,7 +24,7 @@ export const FindingSeverityOverTime = ({
}: FindingSeverityOverTimeProps) => {
const router = useRouter();
const searchParams = useSearchParams();
const [timeRange, setTimeRange] = useState<TimeRange>(DEFAULT_TIME_RANGE);
const [timeRange, setTimeRange] = useState<TimeRange>("5D");
const [data, setData] = useState<LineDataPoint[]>(initialData);
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
@@ -37,12 +37,7 @@ export const FindingSeverityOverTime = ({
dataKey?: string;
}) => {
const params = new URLSearchParams();
// Always filter by FAIL status since this chart shows failed findings
params.set("filter[status__in]", "FAIL");
// Exclude muted findings
params.set("filter[muted]", "false");
params.set("filter[inserted_at]", point.date);
// Add scan_ids filter
if (
@@ -101,6 +96,15 @@ export const FindingSeverityOverTime = ({
// Build line configurations from shared severity configs
const lines: LineConfig[] = [...SEVERITY_LINE_CONFIGS];
// Only add muted line if data contains it
if (data.some((item) => item.muted !== undefined)) {
lines.push({
dataKey: "muted",
color: MUTED_COLOR,
label: "Muted",
});
}
// Calculate x-axis interval based on data length to show all labels without overlap
const getXAxisInterval = (): number => {
const dataLength = data.length;
@@ -2,12 +2,14 @@
import { cn } from "@/lib/utils";
import {
TIME_RANGE_OPTIONS,
type TimeRange,
} from "../_constants/time-range.constants";
const TIME_RANGE_OPTIONS = {
FIVE_DAYS: "5D",
ONE_WEEK: "1W",
ONE_MONTH: "1M",
} as const;
export type { TimeRange };
export type TimeRange =
(typeof TIME_RANGE_OPTIONS)[keyof typeof TIME_RANGE_OPTIONS];
interface TimeRangeSelectorProps {
value: TimeRange;
@@ -1 +0,0 @@
export * from "./time-range.constants";
@@ -1,23 +0,0 @@
export const TIME_RANGE_OPTIONS = {
FIVE_DAYS: "5D",
ONE_WEEK: "1W",
ONE_MONTH: "1M",
} as const;
export type TimeRange =
(typeof TIME_RANGE_OPTIONS)[keyof typeof TIME_RANGE_OPTIONS];
export const TIME_RANGE_DAYS: Record<TimeRange, number> = {
"5D": 5,
"1W": 7,
"1M": 30,
};
export const DEFAULT_TIME_RANGE: TimeRange = "5D";
export const getDateFromForTimeRange = (timeRange: TimeRange): string => {
const days = TIME_RANGE_DAYS[timeRange];
const date = new Date();
date.setDate(date.getDate() - days);
return date.toISOString().split("T")[0];
};
@@ -1,11 +1,10 @@
import { getSeverityTrendsByTimeRange } from "@/actions/overview/severity-trends";
import { getFindingsSeverityTrends } from "@/actions/overview/severity-trends";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/shadcn";
import { pickFilterParams } from "../_lib/filter-params";
import { SSRComponentProps } from "../_types";
import { FindingSeverityOverTime } from "./_components/finding-severity-over-time";
import { FindingSeverityOverTimeSkeleton } from "./_components/finding-severity-over-time.skeleton";
import { DEFAULT_TIME_RANGE } from "./_constants/time-range.constants";
export { FindingSeverityOverTimeSkeleton };
@@ -13,7 +12,7 @@ const EmptyState = ({ message }: { message: string }) => (
<Card variant="base" className="flex h-full min-h-[405px] flex-1 flex-col">
<CardHeader className="flex flex-col gap-4">
<div className="flex items-center justify-between">
<CardTitle>Findings Severity Over Time</CardTitle>
<CardTitle>Finding Severity Over Time</CardTitle>
</div>
</CardHeader>
<CardContent className="flex flex-1 items-center justify-center">
@@ -26,11 +25,7 @@ export const FindingSeverityOverTimeSSR = async ({
searchParams,
}: SSRComponentProps) => {
const filters = pickFilterParams(searchParams);
const result = await getSeverityTrendsByTimeRange({
timeRange: DEFAULT_TIME_RANGE,
filters,
});
const result = await getFindingsSeverityTrends({ filters });
if (result.status === "error") {
return <EmptyState message="Failed to load severity trends data" />;
@@ -44,7 +39,7 @@ export const FindingSeverityOverTimeSSR = async ({
<Card variant="base" className="flex h-full flex-1 flex-col">
<CardHeader className="flex flex-col gap-4">
<div className="flex items-center justify-between">
<CardTitle>Findings Severity Over Time</CardTitle>
<CardTitle>Finding Severity Over Time</CardTitle>
</div>
</CardHeader>
+5 -6
View File
@@ -1,5 +1,3 @@
"use client";
/**
* Client-side Sentry instrumentation
*
@@ -10,12 +8,13 @@
* For runtime-specific configs, see: sentry/sentry.server.config.ts and sentry/sentry.edge.config.ts
*/
import { browserTracingIntegration } from "@sentry/browser";
import * as Sentry from "@sentry/nextjs";
const SENTRY_DSN = process.env.NEXT_PUBLIC_SENTRY_DSN;
// Only initialize Sentry in the browser (not during SSR)
if (typeof window !== "undefined" && SENTRY_DSN) {
// Only initialize Sentry if DSN is configured
if (SENTRY_DSN) {
const isDevelopment = process.env.NEXT_PUBLIC_SENTRY_ENVIRONMENT === "local";
/**
@@ -44,12 +43,12 @@ if (typeof window !== "undefined" && SENTRY_DSN) {
tracesSampleRate: isDevelopment ? 1.0 : 0.5,
profilesSampleRate: isDevelopment ? 1.0 : 0.5,
// 🔌 Integrations - browserTracingIntegration is client-only
// 🔌 Integrations
integrations: [
// 📊 Performance Monitoring: Core Web Vitals + RUM
// Tracks LCP, FID, CLS, INP
// Real User Monitoring captures actual user experience, not synthetic tests
Sentry.browserTracingIntegration({
browserTracingIntegration({
enableLongTask: true, // Detect tasks that block UI (>50ms)
enableInp: true, // Interaction to Next Paint (Core Web Vital)
}),
+8 -34
View File
@@ -68,31 +68,10 @@ const CustomLineTooltip = ({
const typedPayload = payload as unknown as TooltipPayloadItem[];
// Filter payload if a line is selected or hovered
const filteredPayload = filterLine
const displayPayload = filterLine
? typedPayload.filter((item) => item.dataKey === filterLine)
: typedPayload;
// Sort by severity order: critical, high, medium, low, informational
const severityOrder = [
"critical",
"high",
"medium",
"low",
"informational",
] as const;
const displayPayload = [...filteredPayload].sort((a, b) => {
const aIndex = severityOrder.indexOf(
a.dataKey as (typeof severityOrder)[number],
);
const bIndex = severityOrder.indexOf(
b.dataKey as (typeof severityOrder)[number],
);
// Items not in severityOrder go to the end
if (aIndex === -1) return 1;
if (bIndex === -1) return -1;
return aIndex - bIndex;
});
if (displayPayload.length === 0) {
return null;
}
@@ -117,17 +96,12 @@ const CustomLineTooltip = ({
return (
<div key={item.dataKey} className="space-y-1">
<div className="flex items-center justify-between gap-4">
<div className="flex items-center gap-2">
<div
className="h-2 w-2 rounded-full"
style={{ backgroundColor: item.stroke }}
/>
<span className="text-text-neutral-secondary text-sm">
{item.name}
</span>
</div>
<span className="text-text-neutral-primary text-sm font-medium">
<div className="flex items-center gap-2">
<div
className="h-2 w-2 rounded-full"
style={{ backgroundColor: item.stroke }}
/>
<span className="text-text-neutral-primary text-sm">
{item.value}
</span>
</div>
@@ -286,7 +260,7 @@ export function LineChart({
<div className="mt-4 flex flex-col items-start gap-2">
<p className="text-text-neutral-tertiary pl-2 text-xs">
Click to filter by severity
Click to filter by severity.
</p>
<ChartLegend
items={legendItems}
+9 -105
View File
@@ -15,14 +15,6 @@
"strategy": "installed",
"generatedAt": "2025-10-22T12:36:37.962Z"
},
{
"section": "dependencies",
"name": "@aws-sdk/client-bedrock-runtime",
"from": "3.943.0",
"to": "3.943.0",
"strategy": "installed",
"generatedAt": "2025-12-10T11:34:11.122Z"
},
{
"section": "dependencies",
"name": "@heroui/react",
@@ -39,14 +31,6 @@
"strategy": "installed",
"generatedAt": "2025-10-22T12:36:37.962Z"
},
{
"section": "dependencies",
"name": "@internationalized/date",
"from": "3.10.0",
"to": "3.10.0",
"strategy": "installed",
"generatedAt": "2025-12-10T11:34:11.122Z"
},
{
"section": "dependencies",
"name": "@langchain/aws",
@@ -58,10 +42,10 @@
{
"section": "dependencies",
"name": "@langchain/core",
"from": "0.3.78",
"to": "0.3.77",
"from": "0.3.77",
"to": "0.3.78",
"strategy": "installed",
"generatedAt": "2025-12-10T11:34:11.122Z"
"generatedAt": "2025-11-03T07:43:34.628Z"
},
{
"section": "dependencies",
@@ -103,22 +87,6 @@
"strategy": "installed",
"generatedAt": "2025-10-22T12:36:37.962Z"
},
{
"section": "dependencies",
"name": "@radix-ui/react-avatar",
"from": "1.1.11",
"to": "1.1.11",
"strategy": "installed",
"generatedAt": "2025-12-10T11:34:11.122Z"
},
{
"section": "dependencies",
"name": "@radix-ui/react-collapsible",
"from": "1.1.12",
"to": "1.1.12",
"strategy": "installed",
"generatedAt": "2025-12-10T11:34:11.122Z"
},
{
"section": "dependencies",
"name": "@radix-ui/react-dialog",
@@ -159,14 +127,6 @@
"strategy": "installed",
"generatedAt": "2025-11-20T08:20:16.313Z"
},
{
"section": "dependencies",
"name": "@radix-ui/react-scroll-area",
"from": "1.2.10",
"to": "1.2.10",
"strategy": "installed",
"generatedAt": "2025-12-10T11:34:11.122Z"
},
{
"section": "dependencies",
"name": "@radix-ui/react-select",
@@ -191,14 +151,6 @@
"strategy": "installed",
"generatedAt": "2025-10-22T12:36:37.962Z"
},
{
"section": "dependencies",
"name": "@radix-ui/react-tabs",
"from": "1.1.13",
"to": "1.1.13",
"strategy": "installed",
"generatedAt": "2025-12-10T11:34:11.122Z"
},
{
"section": "dependencies",
"name": "@radix-ui/react-toast",
@@ -207,22 +159,6 @@
"strategy": "installed",
"generatedAt": "2025-10-22T12:36:37.962Z"
},
{
"section": "dependencies",
"name": "@radix-ui/react-tooltip",
"from": "1.2.8",
"to": "1.2.8",
"strategy": "installed",
"generatedAt": "2025-12-10T11:34:11.122Z"
},
{
"section": "dependencies",
"name": "@react-aria/i18n",
"from": "3.12.13",
"to": "3.12.13",
"strategy": "installed",
"generatedAt": "2025-12-10T11:34:11.122Z"
},
{
"section": "dependencies",
"name": "@react-aria/ssr",
@@ -239,37 +175,13 @@
"strategy": "installed",
"generatedAt": "2025-10-22T12:36:37.962Z"
},
{
"section": "dependencies",
"name": "@react-stately/utils",
"from": "3.10.8",
"to": "3.10.8",
"strategy": "installed",
"generatedAt": "2025-12-10T11:34:11.122Z"
},
{
"section": "dependencies",
"name": "@react-types/datepicker",
"from": "3.13.2",
"to": "3.13.2",
"strategy": "installed",
"generatedAt": "2025-12-10T11:34:11.122Z"
},
{
"section": "dependencies",
"name": "@react-types/shared",
"from": "3.26.0",
"to": "3.26.0",
"strategy": "installed",
"generatedAt": "2025-12-10T11:34:11.122Z"
},
{
"section": "dependencies",
"name": "@sentry/nextjs",
"from": "10.11.0",
"to": "10.27.0",
"to": "10.11.0",
"strategy": "installed",
"generatedAt": "2025-12-01T10:01:42.332Z"
"generatedAt": "2025-10-22T15:52:15.849Z"
},
{
"section": "dependencies",
@@ -387,9 +299,9 @@
"section": "dependencies",
"name": "js-yaml",
"from": "4.1.0",
"to": "4.1.1",
"to": "4.1.0",
"strategy": "installed",
"generatedAt": "2025-12-01T10:01:42.332Z"
"generatedAt": "2025-10-22T12:36:37.962Z"
},
{
"section": "dependencies",
@@ -415,14 +327,6 @@
"strategy": "installed",
"generatedAt": "2025-10-22T12:36:37.962Z"
},
{
"section": "dependencies",
"name": "nanoid",
"from": "5.1.6",
"to": "5.1.6",
"strategy": "installed",
"generatedAt": "2025-12-10T11:34:11.122Z"
},
{
"section": "dependencies",
"name": "next",
@@ -435,9 +339,9 @@
"section": "dependencies",
"name": "next-auth",
"from": "5.0.0-beta.29",
"to": "5.0.0-beta.30",
"to": "5.0.0-beta.29",
"strategy": "installed",
"generatedAt": "2025-12-01T10:01:42.332Z"
"generatedAt": "2025-10-22T12:36:37.962Z"
},
{
"section": "dependencies",
+5 -5
View File
@@ -20,8 +20,7 @@
"test:e2e:debug": "playwright test --project=chromium --project=sign-up --project=providers --project=invitations --project=scans --debug",
"test:e2e:headed": "playwright test --project=chromium --project=sign-up --project=providers --project=invitations --project=scans --headed",
"test:e2e:report": "playwright show-report",
"test:e2e:install": "playwright install",
"audit:fix": "pnpm audit fix"
"test:e2e:install": "playwright install"
},
"dependencies": {
"@ai-sdk/langchain": "1.0.59",
@@ -57,7 +56,8 @@
"@react-stately/utils": "3.10.8",
"@react-types/datepicker": "3.13.2",
"@react-types/shared": "3.26.0",
"@sentry/nextjs": "10.27.0",
"@sentry/browser": "10.11.0",
"@sentry/nextjs": "10.11.0",
"@tailwindcss/postcss": "4.1.13",
"@tailwindcss/typography": "0.5.16",
"@tanstack/react-table": "8.21.3",
@@ -72,13 +72,13 @@
"framer-motion": "11.18.2",
"intl-messageformat": "10.7.16",
"jose": "5.10.0",
"js-yaml": "4.1.1",
"js-yaml": "4.1.0",
"jwt-decode": "4.0.0",
"lucide-react": "0.543.0",
"marked": "15.0.12",
"nanoid": "5.1.6",
"next": "15.5.7",
"next-auth": "5.0.0-beta.30",
"next-auth": "5.0.0-beta.29",
"next-themes": "0.2.1",
"radix-ui": "1.4.2",
"react": "19.2.1",
+334 -264
View File
File diff suppressed because it is too large Load Diff