diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md
index 497c4d6214..28365d4acb 100644
--- a/.github/pull_request_template.md
+++ b/.github/pull_request_template.md
@@ -14,14 +14,26 @@ Please add a detailed description of how to review this PR.
### Checklist
-- Are there new checks included in this PR? Yes / No
- - If so, do we need to update permissions for the provider? Please review this carefully.
+
+
+Community Checklist
+
+- [ ] This feature/issue is listed in [here](https://github.com/prowler-cloud/prowler/issues?q=sort%3Aupdated-desc+is%3Aissue+is%3Aopen) or roadmap.prowler.com
+- [ ] Is it assigned to me, if not, request it via the issue/feature in [here](https://github.com/prowler-cloud/prowler/issues?q=sort%3Aupdated-desc+is%3Aissue+is%3Aopen) or [Prowler Community Slack](goto.prowler.com/slack)
+
+
+
+
- [ ] Review if the code is being covered by tests.
- [ ] Review if code is being documented following this specification https://github.com/google/styleguide/blob/gh-pages/pyguide.md#38-comments-and-docstrings
- [ ] Review if backport is needed.
- [ ] Review if is needed to change the [Readme.md](https://github.com/prowler-cloud/prowler/blob/master/README.md)
- [ ] Ensure new entries are added to [CHANGELOG.md](https://github.com/prowler-cloud/prowler/blob/master/prowler/CHANGELOG.md), if applicable.
+#### SDK/CLI
+- Are there new checks included in this PR? Yes / No
+ - If so, do we need to update permissions for the provider? Please review this carefully.
+
#### UI
- [ ] All issue/task requirements work as expected on the UI
- [ ] Screenshots/Video of the functionality flow (if applicable) - Mobile (X < 640px)
@@ -30,6 +42,11 @@ Please add a detailed description of how to review this PR.
- [ ] Ensure new entries are added to [CHANGELOG.md](https://github.com/prowler-cloud/prowler/blob/master/ui/CHANGELOG.md), if applicable.
#### API
+- [ ] All issue/task requirements work as expected on the API
+- [ ] Endpoint response output (if applicable)
+- [ ] EXPLAIN ANALYZE output for new/modified queries or indexes (if applicable)
+- [ ] Performance test results (if applicable)
+- [ ] Any other relevant evidence of the implementation (if applicable)
- [ ] Verify if API specs need to be regenerated.
- [ ] Check if version updates are required (e.g., specs, Poetry, etc.).
- [ ] Ensure new entries are added to [CHANGELOG.md](https://github.com/prowler-cloud/prowler/blob/master/api/CHANGELOG.md), if applicable.
diff --git a/.github/workflows/api-bump-version.yml b/.github/workflows/api-bump-version.yml
index cc485a7030..97cdd546ff 100644
--- a/.github/workflows/api-bump-version.yml
+++ b/.github/workflows/api-bump-version.yml
@@ -110,7 +110,7 @@ jobs:
git --no-pager diff
- name: Create PR for next API minor version to master
- uses: peter-evans/create-pull-request@271a8d0340265f705b14b6d32b9829c1cb33d45e # v7.0.8
+ uses: peter-evans/create-pull-request@98357b18bf14b5342f975ff684046ec3b2a07725 # v8.0.0
with:
author: prowler-bot <179230569+prowler-bot@users.noreply.github.com>
token: ${{ secrets.PROWLER_BOT_ACCESS_TOKEN }}
@@ -164,7 +164,7 @@ jobs:
git --no-pager diff
- name: Create PR for first API patch version to version branch
- uses: peter-evans/create-pull-request@271a8d0340265f705b14b6d32b9829c1cb33d45e # v7.0.8
+ uses: peter-evans/create-pull-request@98357b18bf14b5342f975ff684046ec3b2a07725 # v8.0.0
with:
author: prowler-bot <179230569+prowler-bot@users.noreply.github.com>
token: ${{ secrets.PROWLER_BOT_ACCESS_TOKEN }}
@@ -235,7 +235,7 @@ jobs:
git --no-pager diff
- name: Create PR for next API patch version to version branch
- uses: peter-evans/create-pull-request@271a8d0340265f705b14b6d32b9829c1cb33d45e # v7.0.8
+ uses: peter-evans/create-pull-request@98357b18bf14b5342f975ff684046ec3b2a07725 # v8.0.0
with:
author: prowler-bot <179230569+prowler-bot@users.noreply.github.com>
token: ${{ secrets.PROWLER_BOT_ACCESS_TOKEN }}
diff --git a/.github/workflows/api-code-quality.yml b/.github/workflows/api-code-quality.yml
index e0a5306997..0b6131538d 100644
--- a/.github/workflows/api-code-quality.yml
+++ b/.github/workflows/api-code-quality.yml
@@ -37,7 +37,7 @@ jobs:
- name: Check for API changes
id: check-changes
- uses: tj-actions/changed-files@24d32ffd492484c1d75e0c0b894501ddb9d30d62 # v47.0.0
+ uses: tj-actions/changed-files@e0021407031f5be11a464abee9a0776171c79891 # v47.0.1
with:
files: |
api/**
diff --git a/.github/workflows/api-container-build-push.yml b/.github/workflows/api-container-build-push.yml
index c6952c42a2..17693785f7 100644
--- a/.github/workflows/api-container-build-push.yml
+++ b/.github/workflows/api-container-build-push.yml
@@ -102,7 +102,7 @@ jobs:
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Set up Docker Buildx
- uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1
+ uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0
- name: Build and push API container for ${{ matrix.arch }}
id: container-push
@@ -125,13 +125,13 @@ jobs:
steps:
- name: Login to DockerHub
- uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0
+ uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Set up Docker Buildx
- uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1
+ uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0
- name: Create and push manifests for push event
if: github.event_name == 'push'
diff --git a/.github/workflows/api-container-checks.yml b/.github/workflows/api-container-checks.yml
index b5905e5311..d63f75894d 100644
--- a/.github/workflows/api-container-checks.yml
+++ b/.github/workflows/api-container-checks.yml
@@ -32,7 +32,7 @@ jobs:
- name: Check if Dockerfile changed
id: dockerfile-changed
- uses: tj-actions/changed-files@24d32ffd492484c1d75e0c0b894501ddb9d30d62 # v47.0.0
+ uses: tj-actions/changed-files@e0021407031f5be11a464abee9a0776171c79891 # v47.0.1
with:
files: api/Dockerfile
@@ -67,7 +67,7 @@ jobs:
- name: Check for API changes
id: check-changes
- uses: tj-actions/changed-files@24d32ffd492484c1d75e0c0b894501ddb9d30d62 # v47.0.0
+ uses: tj-actions/changed-files@e0021407031f5be11a464abee9a0776171c79891 # v47.0.1
with:
files: api/**
files_ignore: |
@@ -77,7 +77,7 @@ jobs:
- name: Set up Docker Buildx
if: steps.check-changes.outputs.any_changed == 'true'
- uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1
+ uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0
- name: Build container for ${{ matrix.arch }}
if: steps.check-changes.outputs.any_changed == 'true'
diff --git a/.github/workflows/api-security.yml b/.github/workflows/api-security.yml
index 14536a5183..d764f82236 100644
--- a/.github/workflows/api-security.yml
+++ b/.github/workflows/api-security.yml
@@ -37,7 +37,7 @@ jobs:
- name: Check for API changes
id: check-changes
- uses: tj-actions/changed-files@24d32ffd492484c1d75e0c0b894501ddb9d30d62 # v47.0.0
+ uses: tj-actions/changed-files@e0021407031f5be11a464abee9a0776171c79891 # v47.0.1
with:
files: |
api/**
@@ -60,9 +60,7 @@ jobs:
- name: Safety
if: steps.check-changes.outputs.any_changed == 'true'
- # 76352, 76353, 77323 come from SDK, but they cannot upgrade it yet. It does not affect API
- # TODO: Botocore needs urllib3 1.X so we need to ignore these vulnerabilities 77744,77745. Remove this once we upgrade to urllib3 2.X
- run: poetry run safety check --ignore 70612,66963,74429,76352,76353,77323,77744,77745
+ run: poetry run safety check
- name: Vulture
if: steps.check-changes.outputs.any_changed == 'true'
diff --git a/.github/workflows/api-tests.yml b/.github/workflows/api-tests.yml
index e9b2cef4c5..44b4141dc2 100644
--- a/.github/workflows/api-tests.yml
+++ b/.github/workflows/api-tests.yml
@@ -77,7 +77,7 @@ jobs:
- name: Check for API changes
id: check-changes
- uses: tj-actions/changed-files@24d32ffd492484c1d75e0c0b894501ddb9d30d62 # v47.0.0
+ uses: tj-actions/changed-files@e0021407031f5be11a464abee9a0776171c79891 # v47.0.1
with:
files: |
api/**
@@ -100,7 +100,7 @@ jobs:
- name: Upload coverage reports to Codecov
if: steps.check-changes.outputs.any_changed == 'true'
- uses: codecov/codecov-action@5a1091511ad55cbe89839c7260b706298ca349f7 # v5.5.1
+ uses: codecov/codecov-action@671740ac38dd9b0130fbe1cec585b89eea48d3de # v5.5.2
env:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
with:
diff --git a/.github/workflows/docs-bump-version.yml b/.github/workflows/docs-bump-version.yml
index c7bd940a82..dde21b9c1e 100644
--- a/.github/workflows/docs-bump-version.yml
+++ b/.github/workflows/docs-bump-version.yml
@@ -106,7 +106,7 @@ jobs:
git --no-pager diff
- name: Create PR for documentation update to master
- uses: peter-evans/create-pull-request@271a8d0340265f705b14b6d32b9829c1cb33d45e # v7.0.8
+ uses: peter-evans/create-pull-request@98357b18bf14b5342f975ff684046ec3b2a07725 # v8.0.0
with:
author: prowler-bot <179230569+prowler-bot@users.noreply.github.com>
token: ${{ secrets.PROWLER_BOT_ACCESS_TOKEN }}
@@ -161,7 +161,7 @@ jobs:
git --no-pager diff
- name: Create PR for documentation update to version branch
- uses: peter-evans/create-pull-request@271a8d0340265f705b14b6d32b9829c1cb33d45e # v7.0.8
+ uses: peter-evans/create-pull-request@98357b18bf14b5342f975ff684046ec3b2a07725 # v8.0.0
with:
author: prowler-bot <179230569+prowler-bot@users.noreply.github.com>
token: ${{ secrets.PROWLER_BOT_ACCESS_TOKEN }}
@@ -225,7 +225,7 @@ jobs:
git --no-pager diff
- name: Create PR for documentation update to version branch
- uses: peter-evans/create-pull-request@271a8d0340265f705b14b6d32b9829c1cb33d45e # v7.0.8
+ uses: peter-evans/create-pull-request@98357b18bf14b5342f975ff684046ec3b2a07725 # v8.0.0
with:
author: prowler-bot <179230569+prowler-bot@users.noreply.github.com>
token: ${{ secrets.PROWLER_BOT_ACCESS_TOKEN }}
diff --git a/.github/workflows/find-secrets.yml b/.github/workflows/find-secrets.yml
index 4796630e11..c525d8faa4 100644
--- a/.github/workflows/find-secrets.yml
+++ b/.github/workflows/find-secrets.yml
@@ -28,6 +28,6 @@ jobs:
fetch-depth: 0
- name: Scan for secrets with TruffleHog
- uses: trufflesecurity/trufflehog@aade3bff5594fe8808578dd4db3dfeae9bf2abdc # v3.91.1
+ uses: trufflesecurity/trufflehog@ef6e76c3c4023279497fab4721ffa071a722fd05 # v3.92.4
with:
extra_args: '--results=verified,unknown'
diff --git a/.github/workflows/mcp-container-build-push.yml b/.github/workflows/mcp-container-build-push.yml
index 3821f226aa..130ef90129 100644
--- a/.github/workflows/mcp-container-build-push.yml
+++ b/.github/workflows/mcp-container-build-push.yml
@@ -100,7 +100,7 @@ jobs:
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Set up Docker Buildx
- uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1
+ uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0
- name: Build and push MCP container for ${{ matrix.arch }}
id: container-push
@@ -131,13 +131,13 @@ jobs:
steps:
- name: Login to DockerHub
- uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0
+ uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Set up Docker Buildx
- uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1
+ uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0
- name: Create and push manifests for push event
if: github.event_name == 'push'
diff --git a/.github/workflows/mcp-container-checks.yml b/.github/workflows/mcp-container-checks.yml
index 35c4cc6ced..5ea0b6c465 100644
--- a/.github/workflows/mcp-container-checks.yml
+++ b/.github/workflows/mcp-container-checks.yml
@@ -32,7 +32,7 @@ jobs:
- name: Check if Dockerfile changed
id: dockerfile-changed
- uses: tj-actions/changed-files@24d32ffd492484c1d75e0c0b894501ddb9d30d62 # v47.0.0
+ uses: tj-actions/changed-files@e0021407031f5be11a464abee9a0776171c79891 # v47.0.1
with:
files: mcp_server/Dockerfile
@@ -66,7 +66,7 @@ jobs:
- name: Check for MCP changes
id: check-changes
- uses: tj-actions/changed-files@24d32ffd492484c1d75e0c0b894501ddb9d30d62 # v47.0.0
+ uses: tj-actions/changed-files@e0021407031f5be11a464abee9a0776171c79891 # v47.0.1
with:
files: mcp_server/**
files_ignore: |
@@ -75,7 +75,7 @@ jobs:
- name: Set up Docker Buildx
if: steps.check-changes.outputs.any_changed == 'true'
- uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1
+ uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0
- name: Build MCP container for ${{ matrix.arch }}
if: steps.check-changes.outputs.any_changed == 'true'
diff --git a/.github/workflows/pr-check-changelog.yml b/.github/workflows/pr-check-changelog.yml
index 3a8985977b..4a1f8e502a 100644
--- a/.github/workflows/pr-check-changelog.yml
+++ b/.github/workflows/pr-check-changelog.yml
@@ -35,7 +35,7 @@ jobs:
- name: Get changed files
id: changed-files
- uses: tj-actions/changed-files@24d32ffd492484c1d75e0c0b894501ddb9d30d62 # v47.0.0
+ uses: tj-actions/changed-files@e0021407031f5be11a464abee9a0776171c79891 # v47.0.1
with:
files: |
api/**
diff --git a/.github/workflows/pr-conflict-checker.yml b/.github/workflows/pr-conflict-checker.yml
index 6486355b80..c812f4ef19 100644
--- a/.github/workflows/pr-conflict-checker.yml
+++ b/.github/workflows/pr-conflict-checker.yml
@@ -32,7 +32,7 @@ jobs:
- name: Get changed files
id: changed-files
- uses: tj-actions/changed-files@24d32ffd492484c1d75e0c0b894501ddb9d30d62 # v47.0.0
+ uses: tj-actions/changed-files@e0021407031f5be11a464abee9a0776171c79891 # v47.0.1
with:
files: '**'
diff --git a/.github/workflows/prepare-release.yml b/.github/workflows/prepare-release.yml
index a0073277fb..815ade1dec 100644
--- a/.github/workflows/prepare-release.yml
+++ b/.github/workflows/prepare-release.yml
@@ -344,7 +344,7 @@ jobs:
- name: Create PR for API dependency update
if: ${{ env.PATCH_VERSION == '0' }}
- uses: peter-evans/create-pull-request@271a8d0340265f705b14b6d32b9829c1cb33d45e # v7.0.8
+ uses: peter-evans/create-pull-request@98357b18bf14b5342f975ff684046ec3b2a07725 # v8.0.0
with:
token: ${{ secrets.PROWLER_BOT_ACCESS_TOKEN }}
commit-message: 'chore(api): update prowler dependency to ${{ env.BRANCH_NAME }} for release ${{ env.PROWLER_VERSION }}'
@@ -374,7 +374,7 @@ jobs:
no-changelog
- name: Create draft release
- uses: softprops/action-gh-release@6da8fa9354ddfdc4aeace5fc48d7f679b5214090 # v2.4.1
+ uses: softprops/action-gh-release@a06a81a03ee405af7f2048a818ed3f03bbf83c7b # v2.5.0
with:
tag_name: ${{ env.PROWLER_VERSION }}
name: Prowler ${{ env.PROWLER_VERSION }}
diff --git a/.github/workflows/sdk-bump-version.yml b/.github/workflows/sdk-bump-version.yml
index 0383837431..9f25065689 100644
--- a/.github/workflows/sdk-bump-version.yml
+++ b/.github/workflows/sdk-bump-version.yml
@@ -91,7 +91,7 @@ jobs:
git --no-pager diff
- name: Create PR for next minor version to master
- uses: peter-evans/create-pull-request@271a8d0340265f705b14b6d32b9829c1cb33d45e # v7.0.8
+ uses: peter-evans/create-pull-request@98357b18bf14b5342f975ff684046ec3b2a07725 # v8.0.0
with:
author: prowler-bot <179230569+prowler-bot@users.noreply.github.com>
token: ${{ secrets.PROWLER_BOT_ACCESS_TOKEN }}
@@ -139,7 +139,7 @@ jobs:
git --no-pager diff
- name: Create PR for first patch version to version branch
- uses: peter-evans/create-pull-request@271a8d0340265f705b14b6d32b9829c1cb33d45e # v7.0.8
+ uses: peter-evans/create-pull-request@98357b18bf14b5342f975ff684046ec3b2a07725 # v8.0.0
with:
author: prowler-bot <179230569+prowler-bot@users.noreply.github.com>
token: ${{ secrets.PROWLER_BOT_ACCESS_TOKEN }}
@@ -196,7 +196,7 @@ jobs:
git --no-pager diff
- name: Create PR for next patch version to version branch
- uses: peter-evans/create-pull-request@271a8d0340265f705b14b6d32b9829c1cb33d45e # v7.0.8
+ uses: peter-evans/create-pull-request@98357b18bf14b5342f975ff684046ec3b2a07725 # v8.0.0
with:
author: prowler-bot <179230569+prowler-bot@users.noreply.github.com>
token: ${{ secrets.PROWLER_BOT_ACCESS_TOKEN }}
diff --git a/.github/workflows/sdk-code-quality.yml b/.github/workflows/sdk-code-quality.yml
index c52d1a6a8f..068b482f1c 100644
--- a/.github/workflows/sdk-code-quality.yml
+++ b/.github/workflows/sdk-code-quality.yml
@@ -35,7 +35,7 @@ jobs:
- name: Check for SDK changes
id: check-changes
- uses: tj-actions/changed-files@24d32ffd492484c1d75e0c0b894501ddb9d30d62 # v47.0.0
+ uses: tj-actions/changed-files@e0021407031f5be11a464abee9a0776171c79891 # v47.0.1
with:
files: ./**
files_ignore: |
@@ -79,11 +79,11 @@ jobs:
- name: Lint with flake8
if: steps.check-changes.outputs.any_changed == 'true'
- run: poetry run flake8 . --ignore=E266,W503,E203,E501,W605,E128 --exclude contrib,ui,api
+ run: poetry run flake8 . --ignore=E266,W503,E203,E501,W605,E128 --exclude contrib,ui,api,skills
- name: Check format with black
if: steps.check-changes.outputs.any_changed == 'true'
- run: poetry run black --exclude api ui --check .
+ run: poetry run black --exclude api ui skills --check .
- name: Lint with pylint
if: steps.check-changes.outputs.any_changed == 'true'
diff --git a/.github/workflows/sdk-container-build-push.yml b/.github/workflows/sdk-container-build-push.yml
index 22cca521b3..d510ec8568 100644
--- a/.github/workflows/sdk-container-build-push.yml
+++ b/.github/workflows/sdk-container-build-push.yml
@@ -169,7 +169,7 @@ jobs:
AWS_REGION: ${{ env.AWS_REGION }}
- name: Set up Docker Buildx
- uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1
+ uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0
- name: Build and push SDK container for ${{ matrix.arch }}
id: container-push
@@ -193,13 +193,13 @@ jobs:
steps:
- name: Login to DockerHub
- uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0
+ uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Login to Public ECR
- uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0
+ uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
with:
registry: public.ecr.aws
username: ${{ secrets.PUBLIC_ECR_AWS_ACCESS_KEY_ID }}
@@ -208,7 +208,7 @@ jobs:
AWS_REGION: ${{ env.AWS_REGION }}
- name: Set up Docker Buildx
- uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1
+ uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0
- name: Create and push manifests for push event
if: github.event_name == 'push'
diff --git a/.github/workflows/sdk-container-checks.yml b/.github/workflows/sdk-container-checks.yml
index 7b729ab148..b85f0a6ebe 100644
--- a/.github/workflows/sdk-container-checks.yml
+++ b/.github/workflows/sdk-container-checks.yml
@@ -31,7 +31,7 @@ jobs:
- name: Check if Dockerfile changed
id: dockerfile-changed
- uses: tj-actions/changed-files@24d32ffd492484c1d75e0c0b894501ddb9d30d62 # v47.0.0
+ uses: tj-actions/changed-files@e0021407031f5be11a464abee9a0776171c79891 # v47.0.1
with:
files: Dockerfile
@@ -66,7 +66,7 @@ jobs:
- name: Check for SDK changes
id: check-changes
- uses: tj-actions/changed-files@24d32ffd492484c1d75e0c0b894501ddb9d30d62 # v47.0.0
+ uses: tj-actions/changed-files@e0021407031f5be11a464abee9a0776171c79891 # v47.0.1
with:
files: ./**
files_ignore: |
@@ -89,7 +89,7 @@ jobs:
- name: Set up Docker Buildx
if: steps.check-changes.outputs.any_changed == 'true'
- uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1
+ uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0
- name: Build SDK container for ${{ matrix.arch }}
if: steps.check-changes.outputs.any_changed == 'true'
diff --git a/.github/workflows/sdk-refresh-aws-services-regions.yml b/.github/workflows/sdk-refresh-aws-services-regions.yml
index 672242398c..25839f2631 100644
--- a/.github/workflows/sdk-refresh-aws-services-regions.yml
+++ b/.github/workflows/sdk-refresh-aws-services-regions.yml
@@ -50,7 +50,7 @@ jobs:
- name: Create pull request
id: create-pr
- uses: peter-evans/create-pull-request@271a8d0340265f705b14b6d32b9829c1cb33d45e # v7.0.8
+ uses: peter-evans/create-pull-request@98357b18bf14b5342f975ff684046ec3b2a07725 # v8.0.0
with:
token: ${{ secrets.PROWLER_BOT_ACCESS_TOKEN }}
author: 'prowler-bot <179230569+prowler-bot@users.noreply.github.com>'
diff --git a/.github/workflows/sdk-security.yml b/.github/workflows/sdk-security.yml
index cec5502bc9..8a102d34ac 100644
--- a/.github/workflows/sdk-security.yml
+++ b/.github/workflows/sdk-security.yml
@@ -28,9 +28,11 @@ jobs:
- name: Check for SDK changes
id: check-changes
- uses: tj-actions/changed-files@24d32ffd492484c1d75e0c0b894501ddb9d30d62 # v47.0.0
+ uses: tj-actions/changed-files@e0021407031f5be11a464abee9a0776171c79891 # v47.0.1
with:
- files: ./**
+ files:
+ ./**
+ .github/workflows/sdk-security.yml
files_ignore: |
.github/**
prowler/CHANGELOG.md
@@ -70,7 +72,7 @@ jobs:
- name: Security scan with Safety
if: steps.check-changes.outputs.any_changed == 'true'
- run: poetry run safety check --ignore 70612 -r pyproject.toml
+ run: poetry run safety check -r pyproject.toml
- name: Dead code detection with Vulture
if: steps.check-changes.outputs.any_changed == 'true'
diff --git a/.github/workflows/sdk-tests.yml b/.github/workflows/sdk-tests.yml
index bcc407d3bf..d55af082a8 100644
--- a/.github/workflows/sdk-tests.yml
+++ b/.github/workflows/sdk-tests.yml
@@ -35,7 +35,7 @@ jobs:
- name: Check for SDK changes
id: check-changes
- uses: tj-actions/changed-files@24d32ffd492484c1d75e0c0b894501ddb9d30d62 # v47.0.0
+ uses: tj-actions/changed-files@e0021407031f5be11a464abee9a0776171c79891 # v47.0.1
with:
files: ./**
files_ignore: |
@@ -75,7 +75,7 @@ jobs:
- name: Check if AWS files changed
if: steps.check-changes.outputs.any_changed == 'true'
id: changed-aws
- uses: tj-actions/changed-files@24d32ffd492484c1d75e0c0b894501ddb9d30d62 # v47.0.0
+ uses: tj-actions/changed-files@e0021407031f5be11a464abee9a0776171c79891 # v47.0.1
with:
files: |
./prowler/**/aws/**
@@ -189,7 +189,7 @@ jobs:
- name: Upload AWS coverage to Codecov
if: steps.changed-aws.outputs.any_changed == 'true'
- uses: codecov/codecov-action@5a1091511ad55cbe89839c7260b706298ca349f7 # v5.5.1
+ uses: codecov/codecov-action@671740ac38dd9b0130fbe1cec585b89eea48d3de # v5.5.2
env:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
with:
@@ -200,7 +200,7 @@ jobs:
- name: Check if Azure files changed
if: steps.check-changes.outputs.any_changed == 'true'
id: changed-azure
- uses: tj-actions/changed-files@24d32ffd492484c1d75e0c0b894501ddb9d30d62 # v47.0.0
+ uses: tj-actions/changed-files@e0021407031f5be11a464abee9a0776171c79891 # v47.0.1
with:
files: |
./prowler/**/azure/**
@@ -213,7 +213,7 @@ jobs:
- name: Upload Azure coverage to Codecov
if: steps.changed-azure.outputs.any_changed == 'true'
- uses: codecov/codecov-action@5a1091511ad55cbe89839c7260b706298ca349f7 # v5.5.1
+ uses: codecov/codecov-action@671740ac38dd9b0130fbe1cec585b89eea48d3de # v5.5.2
env:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
with:
@@ -224,7 +224,7 @@ jobs:
- name: Check if GCP files changed
if: steps.check-changes.outputs.any_changed == 'true'
id: changed-gcp
- uses: tj-actions/changed-files@24d32ffd492484c1d75e0c0b894501ddb9d30d62 # v47.0.0
+ uses: tj-actions/changed-files@e0021407031f5be11a464abee9a0776171c79891 # v47.0.1
with:
files: |
./prowler/**/gcp/**
@@ -237,7 +237,7 @@ jobs:
- name: Upload GCP coverage to Codecov
if: steps.changed-gcp.outputs.any_changed == 'true'
- uses: codecov/codecov-action@5a1091511ad55cbe89839c7260b706298ca349f7 # v5.5.1
+ uses: codecov/codecov-action@671740ac38dd9b0130fbe1cec585b89eea48d3de # v5.5.2
env:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
with:
@@ -248,7 +248,7 @@ jobs:
- name: Check if Kubernetes files changed
if: steps.check-changes.outputs.any_changed == 'true'
id: changed-kubernetes
- uses: tj-actions/changed-files@24d32ffd492484c1d75e0c0b894501ddb9d30d62 # v47.0.0
+ uses: tj-actions/changed-files@e0021407031f5be11a464abee9a0776171c79891 # v47.0.1
with:
files: |
./prowler/**/kubernetes/**
@@ -261,7 +261,7 @@ jobs:
- name: Upload Kubernetes coverage to Codecov
if: steps.changed-kubernetes.outputs.any_changed == 'true'
- uses: codecov/codecov-action@5a1091511ad55cbe89839c7260b706298ca349f7 # v5.5.1
+ uses: codecov/codecov-action@671740ac38dd9b0130fbe1cec585b89eea48d3de # v5.5.2
env:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
with:
@@ -272,7 +272,7 @@ jobs:
- name: Check if GitHub files changed
if: steps.check-changes.outputs.any_changed == 'true'
id: changed-github
- uses: tj-actions/changed-files@24d32ffd492484c1d75e0c0b894501ddb9d30d62 # v47.0.0
+ uses: tj-actions/changed-files@e0021407031f5be11a464abee9a0776171c79891 # v47.0.1
with:
files: |
./prowler/**/github/**
@@ -285,7 +285,7 @@ jobs:
- name: Upload GitHub coverage to Codecov
if: steps.changed-github.outputs.any_changed == 'true'
- uses: codecov/codecov-action@5a1091511ad55cbe89839c7260b706298ca349f7 # v5.5.1
+ uses: codecov/codecov-action@671740ac38dd9b0130fbe1cec585b89eea48d3de # v5.5.2
env:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
with:
@@ -296,7 +296,7 @@ jobs:
- name: Check if NHN files changed
if: steps.check-changes.outputs.any_changed == 'true'
id: changed-nhn
- uses: tj-actions/changed-files@24d32ffd492484c1d75e0c0b894501ddb9d30d62 # v47.0.0
+ uses: tj-actions/changed-files@e0021407031f5be11a464abee9a0776171c79891 # v47.0.1
with:
files: |
./prowler/**/nhn/**
@@ -309,7 +309,7 @@ jobs:
- name: Upload NHN coverage to Codecov
if: steps.changed-nhn.outputs.any_changed == 'true'
- uses: codecov/codecov-action@5a1091511ad55cbe89839c7260b706298ca349f7 # v5.5.1
+ uses: codecov/codecov-action@671740ac38dd9b0130fbe1cec585b89eea48d3de # v5.5.2
env:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
with:
@@ -320,7 +320,7 @@ jobs:
- name: Check if M365 files changed
if: steps.check-changes.outputs.any_changed == 'true'
id: changed-m365
- uses: tj-actions/changed-files@24d32ffd492484c1d75e0c0b894501ddb9d30d62 # v47.0.0
+ uses: tj-actions/changed-files@e0021407031f5be11a464abee9a0776171c79891 # v47.0.1
with:
files: |
./prowler/**/m365/**
@@ -333,7 +333,7 @@ jobs:
- name: Upload M365 coverage to Codecov
if: steps.changed-m365.outputs.any_changed == 'true'
- uses: codecov/codecov-action@5a1091511ad55cbe89839c7260b706298ca349f7 # v5.5.1
+ uses: codecov/codecov-action@671740ac38dd9b0130fbe1cec585b89eea48d3de # v5.5.2
env:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
with:
@@ -344,7 +344,7 @@ jobs:
- name: Check if IaC files changed
if: steps.check-changes.outputs.any_changed == 'true'
id: changed-iac
- uses: tj-actions/changed-files@24d32ffd492484c1d75e0c0b894501ddb9d30d62 # v47.0.0
+ uses: tj-actions/changed-files@e0021407031f5be11a464abee9a0776171c79891 # v47.0.1
with:
files: |
./prowler/**/iac/**
@@ -357,7 +357,7 @@ jobs:
- name: Upload IaC coverage to Codecov
if: steps.changed-iac.outputs.any_changed == 'true'
- uses: codecov/codecov-action@5a1091511ad55cbe89839c7260b706298ca349f7 # v5.5.1
+ uses: codecov/codecov-action@671740ac38dd9b0130fbe1cec585b89eea48d3de # v5.5.2
env:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
with:
@@ -368,7 +368,7 @@ jobs:
- name: Check if MongoDB Atlas files changed
if: steps.check-changes.outputs.any_changed == 'true'
id: changed-mongodbatlas
- uses: tj-actions/changed-files@24d32ffd492484c1d75e0c0b894501ddb9d30d62 # v47.0.0
+ uses: tj-actions/changed-files@e0021407031f5be11a464abee9a0776171c79891 # v47.0.1
with:
files: |
./prowler/**/mongodbatlas/**
@@ -381,7 +381,7 @@ jobs:
- name: Upload MongoDB Atlas coverage to Codecov
if: steps.changed-mongodbatlas.outputs.any_changed == 'true'
- uses: codecov/codecov-action@5a1091511ad55cbe89839c7260b706298ca349f7 # v5.5.1
+ uses: codecov/codecov-action@671740ac38dd9b0130fbe1cec585b89eea48d3de # v5.5.2
env:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
with:
@@ -392,7 +392,7 @@ jobs:
- name: Check if OCI files changed
if: steps.check-changes.outputs.any_changed == 'true'
id: changed-oraclecloud
- uses: tj-actions/changed-files@24d32ffd492484c1d75e0c0b894501ddb9d30d62 # v47.0.0
+ uses: tj-actions/changed-files@e0021407031f5be11a464abee9a0776171c79891 # v47.0.1
with:
files: |
./prowler/**/oraclecloud/**
@@ -405,7 +405,7 @@ jobs:
- name: Upload OCI coverage to Codecov
if: steps.changed-oraclecloud.outputs.any_changed == 'true'
- uses: codecov/codecov-action@5a1091511ad55cbe89839c7260b706298ca349f7 # v5.5.1
+ uses: codecov/codecov-action@671740ac38dd9b0130fbe1cec585b89eea48d3de # v5.5.2
env:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
with:
@@ -416,7 +416,7 @@ jobs:
- name: Check if Lib files changed
if: steps.check-changes.outputs.any_changed == 'true'
id: changed-lib
- uses: tj-actions/changed-files@24d32ffd492484c1d75e0c0b894501ddb9d30d62 # v47.0.0
+ uses: tj-actions/changed-files@e0021407031f5be11a464abee9a0776171c79891 # v47.0.1
with:
files: |
./prowler/lib/**
@@ -429,7 +429,7 @@ jobs:
- name: Upload Lib coverage to Codecov
if: steps.changed-lib.outputs.any_changed == 'true'
- uses: codecov/codecov-action@5a1091511ad55cbe89839c7260b706298ca349f7 # v5.5.1
+ uses: codecov/codecov-action@671740ac38dd9b0130fbe1cec585b89eea48d3de # v5.5.2
env:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
with:
@@ -440,7 +440,7 @@ jobs:
- name: Check if Config files changed
if: steps.check-changes.outputs.any_changed == 'true'
id: changed-config
- uses: tj-actions/changed-files@24d32ffd492484c1d75e0c0b894501ddb9d30d62 # v47.0.0
+ uses: tj-actions/changed-files@e0021407031f5be11a464abee9a0776171c79891 # v47.0.1
with:
files: |
./prowler/config/**
@@ -453,7 +453,7 @@ jobs:
- name: Upload Config coverage to Codecov
if: steps.changed-config.outputs.any_changed == 'true'
- uses: codecov/codecov-action@5a1091511ad55cbe89839c7260b706298ca349f7 # v5.5.1
+ uses: codecov/codecov-action@671740ac38dd9b0130fbe1cec585b89eea48d3de # v5.5.2
env:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
with:
diff --git a/.github/workflows/ui-bump-version.yml b/.github/workflows/ui-bump-version.yml
index c25b2809a7..7c576eed55 100644
--- a/.github/workflows/ui-bump-version.yml
+++ b/.github/workflows/ui-bump-version.yml
@@ -90,7 +90,7 @@ jobs:
git --no-pager diff
- name: Create PR for next minor version to master
- uses: peter-evans/create-pull-request@271a8d0340265f705b14b6d32b9829c1cb33d45e # v7.0.8
+ uses: peter-evans/create-pull-request@98357b18bf14b5342f975ff684046ec3b2a07725 # v8.0.0
with:
author: prowler-bot <179230569+prowler-bot@users.noreply.github.com>
token: ${{ secrets.PROWLER_BOT_ACCESS_TOKEN }}
@@ -140,7 +140,7 @@ jobs:
git --no-pager diff
- name: Create PR for first patch version to version branch
- uses: peter-evans/create-pull-request@271a8d0340265f705b14b6d32b9829c1cb33d45e # v7.0.8
+ uses: peter-evans/create-pull-request@98357b18bf14b5342f975ff684046ec3b2a07725 # v8.0.0
with:
author: prowler-bot <179230569+prowler-bot@users.noreply.github.com>
token: ${{ secrets.PROWLER_BOT_ACCESS_TOKEN }}
@@ -199,7 +199,7 @@ jobs:
git --no-pager diff
- name: Create PR for next patch version to version branch
- uses: peter-evans/create-pull-request@271a8d0340265f705b14b6d32b9829c1cb33d45e # v7.0.8
+ uses: peter-evans/create-pull-request@98357b18bf14b5342f975ff684046ec3b2a07725 # v8.0.0
with:
author: prowler-bot <179230569+prowler-bot@users.noreply.github.com>
token: ${{ secrets.PROWLER_BOT_ACCESS_TOKEN }}
diff --git a/.github/workflows/ui-container-build-push.yml b/.github/workflows/ui-container-build-push.yml
index cfec354e89..6f8c232185 100644
--- a/.github/workflows/ui-container-build-push.yml
+++ b/.github/workflows/ui-container-build-push.yml
@@ -104,7 +104,7 @@ jobs:
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Set up Docker Buildx
- uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1
+ uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0
- name: Build and push UI container for ${{ matrix.arch }}
id: container-push
@@ -130,13 +130,13 @@ jobs:
steps:
- name: Login to DockerHub
- uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0
+ uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Set up Docker Buildx
- uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1
+ uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0
- name: Create and push manifests for push event
if: github.event_name == 'push'
diff --git a/.github/workflows/ui-container-checks.yml b/.github/workflows/ui-container-checks.yml
index ac41eaab81..8329132450 100644
--- a/.github/workflows/ui-container-checks.yml
+++ b/.github/workflows/ui-container-checks.yml
@@ -32,7 +32,7 @@ jobs:
- name: Check if Dockerfile changed
id: dockerfile-changed
- uses: tj-actions/changed-files@24d32ffd492484c1d75e0c0b894501ddb9d30d62 # v47.0.0
+ uses: tj-actions/changed-files@e0021407031f5be11a464abee9a0776171c79891 # v47.0.1
with:
files: ui/Dockerfile
@@ -67,7 +67,7 @@ jobs:
- name: Check for UI changes
id: check-changes
- uses: tj-actions/changed-files@24d32ffd492484c1d75e0c0b894501ddb9d30d62 # v47.0.0
+ uses: tj-actions/changed-files@e0021407031f5be11a464abee9a0776171c79891 # v47.0.1
with:
files: ui/**
files_ignore: |
@@ -76,7 +76,7 @@ jobs:
- name: Set up Docker Buildx
if: steps.check-changes.outputs.any_changed == 'true'
- uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1
+ uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0
- name: Build UI container for ${{ matrix.arch }}
if: steps.check-changes.outputs.any_changed == 'true'
diff --git a/.github/workflows/ui-e2e-tests.yml b/.github/workflows/ui-e2e-tests.yml
index db35056879..054bd69f6e 100644
--- a/.github/workflows/ui-e2e-tests.yml
+++ b/.github/workflows/ui-e2e-tests.yml
@@ -114,7 +114,7 @@ jobs:
echo "All database fixtures loaded successfully!"
'
- name: Setup Node.js environment
- uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
+ uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
with:
node-version: '20.x'
- name: Setup pnpm
@@ -126,7 +126,7 @@ jobs:
shell: bash
run: echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV
- name: Setup pnpm cache
- uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
+ uses: actions/cache@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1
with:
path: ${{ env.STORE_PATH }}
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('ui/pnpm-lock.yaml') }}
@@ -139,7 +139,7 @@ jobs:
working-directory: ./ui
run: pnpm run build
- name: Cache Playwright browsers
- uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
+ uses: actions/cache@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1
id: playwright-cache
with:
path: ~/.cache/ms-playwright
@@ -154,7 +154,7 @@ jobs:
working-directory: ./ui
run: pnpm run test:e2e
- name: Upload test reports
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
+ uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
if: failure()
with:
name: playwright-report
diff --git a/.github/workflows/ui-tests.yml b/.github/workflows/ui-tests.yml
index 530b3e1212..c8382f6711 100644
--- a/.github/workflows/ui-tests.yml
+++ b/.github/workflows/ui-tests.yml
@@ -34,7 +34,7 @@ jobs:
- name: Check for UI changes
id: check-changes
- uses: tj-actions/changed-files@24d32ffd492484c1d75e0c0b894501ddb9d30d62 # v47.0.0
+ uses: tj-actions/changed-files@e0021407031f5be11a464abee9a0776171c79891 # v47.0.1
with:
files: |
ui/**
@@ -45,7 +45,7 @@ jobs:
- name: Setup Node.js ${{ env.NODE_VERSION }}
if: steps.check-changes.outputs.any_changed == 'true'
- uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
+ uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
with:
node-version: ${{ env.NODE_VERSION }}
@@ -63,7 +63,7 @@ jobs:
- name: Setup pnpm cache
if: steps.check-changes.outputs.any_changed == 'true'
- uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
+ uses: actions/cache@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1
with:
path: ${{ env.STORE_PATH }}
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('ui/pnpm-lock.yaml') }}
diff --git a/.gitignore b/.gitignore
index e1f49be87c..ffad8a7d37 100644
--- a/.gitignore
+++ b/.gitignore
@@ -82,6 +82,9 @@ continue.json
.continuerc
.continuerc.json
+# AI Coding Assistants - OpenCode
+opencode.json
+
# AI Coding Assistants - GitHub Copilot
.copilot/
.github/copilot/
@@ -152,3 +155,9 @@ CLAUDE.md
# Compliance report
*.pdf
+
+# AI Skills symlinks (generated by skills/setup.sh)
+.claude/skills
+.codex/skills
+.github/skills
+.gemini/skills
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index ea9954f082..24f8f0f211 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -34,6 +34,7 @@ repos:
rev: v2.3.1
hooks:
- id: autoflake
+ exclude: ^skills/
args:
[
"--in-place",
@@ -45,18 +46,20 @@ repos:
rev: 5.13.2
hooks:
- id: isort
+ exclude: ^skills/
args: ["--profile", "black"]
- repo: https://github.com/psf/black
rev: 24.4.2
hooks:
- id: black
+ exclude: ^skills/
- repo: https://github.com/pycqa/flake8
rev: 7.0.0
hooks:
- id: flake8
- exclude: contrib
+ exclude: (contrib|^skills/)
args: ["--ignore=E266,W503,E203,E501,W605"]
- repo: https://github.com/python-poetry/poetry
@@ -109,7 +112,7 @@ repos:
- id: bandit
name: bandit
description: "Bandit is a tool for finding common security issues in Python code"
- entry: bash -c 'bandit -q -lll -x '*_test.py,./contrib/,./.venv/' -r .'
+ entry: bash -c 'bandit -q -lll -x '*_test.py,./contrib/,./.venv/,./skills/' -r .'
language: system
files: '.*\.py'
@@ -123,7 +126,7 @@ repos:
- id: vulture
name: vulture
description: "Vulture finds unused code in Python programs."
- entry: bash -c 'vulture --exclude "contrib,.venv,api/src/backend/api/tests/,api/src/backend/conftest.py,api/src/backend/tasks/tests/" --min-confidence 100 .'
+ entry: bash -c 'vulture --exclude "contrib,.venv,api/src/backend/api/tests/,api/src/backend/conftest.py,api/src/backend/tasks/tests/,skills/" --min-confidence 100 .'
language: system
files: '.*\.py'
diff --git a/AGENTS.md b/AGENTS.md
index c6a6027c18..a982b82fae 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -2,109 +2,83 @@
## How to Use This Guide
-- Start here for cross-project norms, Prowler is a monorepo with several components. Every component should have an `AGENTS.md` file that contains the guidelines for the agents in that component. The file is located beside the code you are touching (e.g. `api/AGENTS.md`, `ui/AGENTS.md`, `prowler/AGENTS.md`).
-- Follow the stricter rule when guidance conflicts; component docs override this file for their scope.
-- Keep instructions synchronized. When you add new workflows or scripts, update both, the relevant component `AGENTS.md` and this file if they apply broadly.
+- Start here for cross-project norms. Prowler is a monorepo with several components.
+- Each component has an `AGENTS.md` file with specific guidelines (e.g., `api/AGENTS.md`, `ui/AGENTS.md`).
+- Component docs override this file when guidance conflicts.
+
+## Available Skills
+
+Use these skills for detailed patterns on-demand:
+
+### Generic Skills (Any Project)
+| Skill | Description | URL |
+|-------|-------------|-----|
+| `typescript` | Const types, flat interfaces, utility types | [SKILL.md](skills/typescript/SKILL.md) |
+| `react-19` | No useMemo/useCallback, React Compiler | [SKILL.md](skills/react-19/SKILL.md) |
+| `nextjs-15` | App Router, Server Actions, streaming | [SKILL.md](skills/nextjs-15/SKILL.md) |
+| `tailwind-4` | cn() utility, no var() in className | [SKILL.md](skills/tailwind-4/SKILL.md) |
+| `playwright` | Page Object Model, MCP workflow, selectors | [SKILL.md](skills/playwright/SKILL.md) |
+| `pytest` | Fixtures, mocking, markers, parametrize | [SKILL.md](skills/pytest/SKILL.md) |
+| `django-drf` | ViewSets, Serializers, Filters | [SKILL.md](skills/django-drf/SKILL.md) |
+| `zod-4` | New API (z.email(), z.uuid()) | [SKILL.md](skills/zod-4/SKILL.md) |
+| `zustand-5` | Persist, selectors, slices | [SKILL.md](skills/zustand-5/SKILL.md) |
+| `ai-sdk-5` | UIMessage, streaming, LangChain | [SKILL.md](skills/ai-sdk-5/SKILL.md) |
+
+### Prowler-Specific Skills
+| Skill | Description | URL |
+|-------|-------------|-----|
+| `prowler` | Project overview, component navigation | [SKILL.md](skills/prowler/SKILL.md) |
+| `prowler-api` | Django + RLS + JSON:API patterns | [SKILL.md](skills/prowler-api/SKILL.md) |
+| `prowler-ui` | Next.js + shadcn conventions | [SKILL.md](skills/prowler-ui/SKILL.md) |
+| `prowler-sdk-check` | Create new security checks | [SKILL.md](skills/prowler-sdk-check/SKILL.md) |
+| `prowler-mcp` | MCP server tools and models | [SKILL.md](skills/prowler-mcp/SKILL.md) |
+| `prowler-test-sdk` | SDK testing (pytest + moto) | [SKILL.md](skills/prowler-test-sdk/SKILL.md) |
+| `prowler-test-api` | API testing (pytest-django + RLS) | [SKILL.md](skills/prowler-test-api/SKILL.md) |
+| `prowler-test-ui` | E2E testing (Playwright) | [SKILL.md](skills/prowler-test-ui/SKILL.md) |
+| `prowler-compliance` | Compliance framework structure | [SKILL.md](skills/prowler-compliance/SKILL.md) |
+| `prowler-provider` | Add new cloud providers | [SKILL.md](skills/prowler-provider/SKILL.md) |
+| `prowler-pr` | Pull request conventions | [SKILL.md](skills/prowler-pr/SKILL.md) |
+| `prowler-docs` | Documentation style guide | [SKILL.md](skills/prowler-docs/SKILL.md) |
+| `skill-creator` | Create new AI agent skills | [SKILL.md](skills/skill-creator/SKILL.md) |
+
+---
## Project Overview
-Prowler is an open-source cloud security assessment tool that supports multiple cloud providers (AWS, Azure, GCP, Kubernetes, GitHub, M365, etc.). The project consists in a monorepo with the following main components:
+Prowler is an open-source cloud security assessment tool supporting AWS, Azure, GCP, Kubernetes, GitHub, M365, and more.
-- **Prowler SDK**: Python SDK, includes the Prowler CLI, providers, services, checks, compliances, config, etc. (`prowler/`)
-- **Prowler API**: Django-based REST API backend (`api/`)
-- **Prowler UI**: Next.js frontend application (`ui/`)
-- **Prowler MCP Server**: Model Context Protocol server that gives access to the entire Prowler ecosystem for LLMs (`mcp_server/`)
-- **Prowler Dashboard**: Prowler CLI feature that allows to visualize the results of the scans in a simple dashboard (`dashboard/`)
+| Component | Location | Tech Stack |
+|-----------|----------|------------|
+| SDK | `prowler/` | Python 3.9+, Poetry |
+| API | `api/` | Django 5.1, DRF, Celery |
+| UI | `ui/` | Next.js 15, React 19, Tailwind 4 |
+| MCP Server | `mcp_server/` | FastMCP, Python 3.12+ |
+| Dashboard | `dashboard/` | Dash, Plotly |
-### Project Structure (Key Folders & Files)
-
-- `prowler/`: Main source code for Prowler SDK (CLI, providers, services, checks, compliances, config, etc.)
-- `api/`: Django-based REST API backend components
-- `ui/`: Next.js frontend application
-- `mcp_server/`: Model Context Protocol server that gives access to the entire Prowler ecosystem for LLMs
-- `dashboard/`: Prowler CLI feature that allows to visualize the results of the scans in a simple dashboard
-- `docs/`: Documentation
-- `examples/`: Example output formats for providers and scripts
-- `permissions/`: Permission-related files and policies
-- `contrib/`: Community-contributed scripts or modules
-- `tests/`: Prowler SDK test suite
-- `docker-compose.yml`: Docker compose file to run the Prowler App (API + UI) production environment
-- `docker-compose-dev.yml`: Docker compose file to run the Prowler App (API + UI) development environment
-- `pyproject.toml`: Poetry Prowler SDK project file
-- `.pre-commit-config.yaml`: Pre-commit hooks configuration
-- `Makefile`: Makefile to run the project
-- `LICENSE`: License file
-- `README.md`: README file
-- `CONTRIBUTING.md`: Contributing guide
+---
## Python Development
-Most of the code is written in Python, so the main files in the root are focused on Python code.
-
-### Poetry Dev Environment
-
-For developing in Python we recommend using `poetry` to manage the dependencies. The minimal version is `2.1.1`. So it is recommended to run all commands using `poetry run ...`.
-
-To install the core dependencies to develop it is needed to run `poetry install --with dev`.
-
-### Pre-commit hooks
-
-The project has pre-commit hooks to lint and format the code. They are installed by running `poetry run pre-commit install`.
-
-When commiting a change, the hooks will be run automatically. Some of them are:
-
-- Code formatting (black, isort)
-- Linting (flake8, pylint)
-- Security checks (bandit, safety, trufflehog)
-- YAML/JSON validation
-- Poetry lock file validation
-
-
-### Linting and Formatting
-
-We use the following tools to lint and format the code:
-
-- `flake8`: for linting the code
-- `black`: for formatting the code
-- `pylint`: for linting the code
-
-You can run all using the `make` command:
```bash
+# Setup
+poetry install --with dev
+poetry run pre-commit install
+
+# Code quality
poetry run make lint
poetry run make format
+poetry run pre-commit run --all-files
```
-Or they will be run automatically when you commit your changes using pre-commit hooks.
+---
## Commit & Pull Request Guidelines
-For the commit messages and pull requests name follow the conventional-commit style.
+Follow conventional-commit style: `[scope]: `
-Befire creating a pull request, complete the checklist in `.github/pull_request_template.md`. Summaries should explain deployment impact, highlight review steps, and note changelog or permission updates. Run all relevant tests and linters before requesting review and link screenshots for UI or dashboard changes.
+**Types:** `feat`, `fix`, `docs`, `chore`, `perf`, `refactor`, `style`, `test`
-### Conventional Commit Style
-
-The Conventional Commits specification is a lightweight convention on top of commit messages. It provides an easy set of rules for creating an explicit commit history; which makes it easier to write automated tools on top of.
-
-The commit message should be structured as follows:
-
-```
-[optional scope]:
-
-[optional body]
-
-[optional footer(s)]
-```
-
-Any line of the commit message cannot be longer 100 characters! This allows the message to be easier to read on GitHub as well as in various git tools
-
-#### Commit Types
-
-- **feat**: code change introuce new functionality to the application
-- **fix**: code change that solve a bug in the codebase
-- **docs**: documentation only changes
-- **chore**: changes related to the build process or auxiliary tools and libraries, that do not affect the application's functionality
-- **perf**: code change that improves performance
-- **refactor**: code change that neither fixes a bug nor adds a feature
-- **style**: changes that do not affect the meaning of the code (white-space, formatting, missing semi-colons, etc)
-- **test**: adding missing tests or correcting existing tests
+Before creating a PR:
+1. Complete checklist in `.github/pull_request_template.md`
+2. Run all relevant tests and linters
+3. Link screenshots for UI changes
diff --git a/README.md b/README.md
index a1e71e23be..bdf08ee20a 100644
--- a/README.md
+++ b/README.md
@@ -310,6 +310,45 @@ And many more environments.

+# 🤖 AI Skills for Development
+
+Prowler includes a comprehensive set of **AI Skills** that help AI coding assistants understand Prowler's codebase patterns and conventions.
+
+## What are AI Skills?
+
+Skills are structured instructions that give AI assistants the context they need to write code that follows Prowler's standards. They include:
+
+- **Coding patterns** for each component (SDK, API, UI, MCP Server)
+- **Testing conventions** (pytest, Playwright)
+- **Architecture guidelines** (Clean Architecture, RLS patterns)
+- **Framework-specific rules** (React 19, Next.js 15, Django DRF, Tailwind 4)
+
+## Available Skills
+
+| Category | Skills |
+|----------|--------|
+| **Generic** | `typescript`, `react-19`, `nextjs-15`, `tailwind-4`, `playwright`, `pytest`, `django-drf`, `zod-4`, `zustand-5`, `ai-sdk-5` |
+| **Prowler** | `prowler`, `prowler-api`, `prowler-ui`, `prowler-mcp`, `prowler-sdk-check`, `prowler-test-ui`, `prowler-test-api`, `prowler-test-sdk`, `prowler-compliance`, `prowler-provider`, `prowler-pr`, `prowler-docs` |
+
+## Setup
+
+```bash
+./skills/setup.sh
+```
+
+This configures skills for AI coding assistants that follow the [agentskills.io](https://agentskills.io) standard:
+
+| Tool | Configuration |
+|------|---------------|
+| **Claude Code** | `.claude/skills/` (symlink) |
+| **OpenCode** | `.claude/skills/` (symlink) |
+| **Codex (OpenAI)** | `.codex/skills/` (symlink) |
+| **GitHub Copilot** | `.github/skills/` (symlink) |
+| **Gemini CLI** | `.gemini/skills/` (symlink) |
+
+> **Note:** Restart your AI coding assistant after running setup to load the skills.
+> Gemini CLI requires `experimental.skills` enabled in settings.
+
# 📖 Documentation
For installation instructions, usage details, tutorials, and the Developer Guide, visit https://docs.prowler.com/
diff --git a/api/AGENTS.md b/api/AGENTS.md
new file mode 100644
index 0000000000..9ae73401c6
--- /dev/null
+++ b/api/AGENTS.md
@@ -0,0 +1,137 @@
+# Prowler API - AI Agent Ruleset
+
+> **Skills Reference**: For detailed patterns, use these skills:
+> - [`prowler-api`](../skills/prowler-api/SKILL.md) - Models, Serializers, Views, RLS patterns
+> - [`prowler-test-api`](../skills/prowler-test-api/SKILL.md) - Testing patterns (pytest-django)
+> - [`django-drf`](../skills/django-drf/SKILL.md) - Generic DRF patterns
+> - [`pytest`](../skills/pytest/SKILL.md) - Generic pytest patterns
+
+## CRITICAL RULES - NON-NEGOTIABLE
+
+### Models
+- ALWAYS: UUIDv4 PKs, `inserted_at`/`updated_at` timestamps, `JSONAPIMeta` class
+- ALWAYS: Inherit from `RowLevelSecurityProtectedModel` for tenant-scoped data
+- NEVER: Auto-increment integer PKs, models without tenant isolation
+
+### Serializers
+- ALWAYS: Separate serializers for Create/Update operations
+- ALWAYS: Inherit from `RLSSerializer` for tenant-scoped models
+- NEVER: Write logic in serializers (use services/utils)
+
+### Views
+- ALWAYS: Inherit from `BaseRLSViewSet` for tenant-scoped resources
+- ALWAYS: Define `filterset_class`, use `@extend_schema` for OpenAPI
+- NEVER: Raw SQL queries, business logic in views
+
+### Row-Level Security (RLS)
+- ALWAYS: Use `rls_transaction(tenant_id)` context manager
+- NEVER: Query across tenants, trust client-provided tenant_id
+
+### Celery Tasks
+- ALWAYS: `@shared_task` with `name`, `queue`, `RLSTask` base class
+- NEVER: Long-running ops in views, request context in tasks
+
+---
+
+## DECISION TREES
+
+### Serializer Selection
+```
+Read → Serializer
+Create → CreateSerializer
+Update → UpdateSerializer
+Nested read → IncludeSerializer
+```
+
+### Task vs View
+```
+< 100ms → View
+> 100ms or external API → Celery task
+Needs retry → Celery task
+```
+
+---
+
+## TECH STACK
+
+Django 5.1.x | DRF 3.15.x | djangorestframework-jsonapi 7.x | Celery 5.4.x | PostgreSQL 16 | pytest 8.x
+
+---
+
+## PROJECT STRUCTURE
+
+```
+api/src/backend/
+├── api/ # Main Django app
+│ ├── v1/ # API version 1 (views, serializers, urls)
+│ ├── models.py # Django models
+│ ├── filters.py # FilterSet classes
+│ ├── base_views.py # Base ViewSet classes
+│ ├── rls.py # Row-Level Security
+│ └── tests/ # Unit tests
+├── config/ # Django configuration
+└── tasks/ # Celery tasks
+```
+
+---
+
+## COMMANDS
+
+```bash
+# Development
+poetry run python src/backend/manage.py runserver
+poetry run celery -A config.celery worker -l INFO
+
+# Database
+poetry run python src/backend/manage.py makemigrations
+poetry run python src/backend/manage.py migrate
+
+# Testing & Linting
+poetry run pytest -x --tb=short
+poetry run make lint
+```
+
+---
+
+## QA CHECKLIST
+
+- [ ] `poetry run pytest` passes
+- [ ] `poetry run make lint` passes
+- [ ] Migrations created if models changed
+- [ ] New endpoints have `@extend_schema` decorators
+- [ ] RLS properly applied for tenant data
+- [ ] Tests cover success and error cases
+
+---
+
+## NAMING CONVENTIONS
+
+| Entity | Pattern | Example |
+|--------|---------|---------|
+| Serializer (read) | `Serializer` | `ProviderSerializer` |
+| Serializer (create) | `CreateSerializer` | `ProviderCreateSerializer` |
+| Serializer (update) | `UpdateSerializer` | `ProviderUpdateSerializer` |
+| Filter | `Filter` | `ProviderFilter` |
+| ViewSet | `ViewSet` | `ProviderViewSet` |
+| Task | `__task` | `sync_provider_resources_task` |
+
+---
+
+## API CONVENTIONS (JSON:API)
+
+```json
+{
+ "data": {
+ "type": "providers",
+ "id": "uuid",
+ "attributes": { "name": "value" },
+ "relationships": { "tenant": { "data": { "type": "tenants", "id": "uuid" } } }
+ }
+}
+```
+
+- Content-Type: `application/vnd.api+json`
+- Pagination: `?page[number]=1&page[size]=20`
+- Filtering: `?filter[field]=value`, `?filter[field__in]=val1,val2`
+- Sorting: `?sort=field`, `?sort=-field`
+- Including: `?include=provider,findings`
diff --git a/api/CHANGELOG.md b/api/CHANGELOG.md
index 8cb9e73f52..48a7753832 100644
--- a/api/CHANGELOG.md
+++ b/api/CHANGELOG.md
@@ -5,7 +5,16 @@ All notable changes to the **Prowler API** are documented in this file.
## [1.18.0] (Prowler UNRELEASED)
### Added
+- `/api/v1/overviews/compliance-watchlist` to retrieve the compliance watchlist [(#9596)](https://github.com/prowler-cloud/prowler/pull/9596)
- Support AlibabaCloud provider [(#9485)](https://github.com/prowler-cloud/prowler/pull/9485)
+- `provider_id` and `provider_id__in` filter aliases for findings endpoints to enable consistent frontend parameter naming [(#9701)](https://github.com/prowler-cloud/prowler/pull/9701)
+
+---
+
+## [1.17.2] (Prowler v5.16.2)
+
+### Security
+- Updated dependencies to patch security vulnerabilities: Django 5.1.15 (CVE-2025-64460, CVE-2025-13372), Werkzeug 3.1.4 (CVE-2025-66221), sqlparse 0.5.5 (PVE-2025-82038), fonttools 4.60.2 (CVE-2025-66034) [(#9730)](https://github.com/prowler-cloud/prowler/pull/9730)
---
diff --git a/api/poetry.lock b/api/poetry.lock
index 8779ad068a..bde85eb28a 100644
--- a/api/poetry.lock
+++ b/api/poetry.lock
@@ -1,4 +1,4 @@
-# This file is automatically @generated by Poetry 2.1.3 and should not be changed by hand.
+# This file is automatically @generated by Poetry 2.2.1 and should not be changed by hand.
[[package]]
name = "about-time"
@@ -38,98 +38,132 @@ files = [
[[package]]
name = "aiohttp"
-version = "3.12.15"
+version = "3.13.3"
description = "Async http client/server framework (asyncio)"
optional = false
python-versions = ">=3.9"
groups = ["main"]
files = [
- {file = "aiohttp-3.12.15-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:b6fc902bff74d9b1879ad55f5404153e2b33a82e72a95c89cec5eb6cc9e92fbc"},
- {file = "aiohttp-3.12.15-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:098e92835b8119b54c693f2f88a1dec690e20798ca5f5fe5f0520245253ee0af"},
- {file = "aiohttp-3.12.15-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:40b3fee496a47c3b4a39a731954c06f0bd9bd3e8258c059a4beb76ac23f8e421"},
- {file = "aiohttp-3.12.15-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2ce13fcfb0bb2f259fb42106cdc63fa5515fb85b7e87177267d89a771a660b79"},
- {file = "aiohttp-3.12.15-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3beb14f053222b391bf9cf92ae82e0171067cc9c8f52453a0f1ec7c37df12a77"},
- {file = "aiohttp-3.12.15-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4c39e87afe48aa3e814cac5f535bc6199180a53e38d3f51c5e2530f5aa4ec58c"},
- {file = "aiohttp-3.12.15-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d5f1b4ce5bc528a6ee38dbf5f39bbf11dd127048726323b72b8e85769319ffc4"},
- {file = "aiohttp-3.12.15-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1004e67962efabbaf3f03b11b4c43b834081c9e3f9b32b16a7d97d4708a9abe6"},
- {file = "aiohttp-3.12.15-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8faa08fcc2e411f7ab91d1541d9d597d3a90e9004180edb2072238c085eac8c2"},
- {file = "aiohttp-3.12.15-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:fe086edf38b2222328cdf89af0dde2439ee173b8ad7cb659b4e4c6f385b2be3d"},
- {file = "aiohttp-3.12.15-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:79b26fe467219add81d5e47b4a4ba0f2394e8b7c7c3198ed36609f9ba161aecb"},
- {file = "aiohttp-3.12.15-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:b761bac1192ef24e16706d761aefcb581438b34b13a2f069a6d343ec8fb693a5"},
- {file = "aiohttp-3.12.15-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:e153e8adacfe2af562861b72f8bc47f8a5c08e010ac94eebbe33dc21d677cd5b"},
- {file = "aiohttp-3.12.15-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:fc49c4de44977aa8601a00edbf157e9a421f227aa7eb477d9e3df48343311065"},
- {file = "aiohttp-3.12.15-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:2776c7ec89c54a47029940177e75c8c07c29c66f73464784971d6a81904ce9d1"},
- {file = "aiohttp-3.12.15-cp310-cp310-win32.whl", hash = "sha256:2c7d81a277fa78b2203ab626ced1487420e8c11a8e373707ab72d189fcdad20a"},
- {file = "aiohttp-3.12.15-cp310-cp310-win_amd64.whl", hash = "sha256:83603f881e11f0f710f8e2327817c82e79431ec976448839f3cd05d7afe8f830"},
- {file = "aiohttp-3.12.15-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:d3ce17ce0220383a0f9ea07175eeaa6aa13ae5a41f30bc61d84df17f0e9b1117"},
- {file = "aiohttp-3.12.15-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:010cc9bbd06db80fe234d9003f67e97a10fe003bfbedb40da7d71c1008eda0fe"},
- {file = "aiohttp-3.12.15-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3f9d7c55b41ed687b9d7165b17672340187f87a773c98236c987f08c858145a9"},
- {file = "aiohttp-3.12.15-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bc4fbc61bb3548d3b482f9ac7ddd0f18c67e4225aaa4e8552b9f1ac7e6bda9e5"},
- {file = "aiohttp-3.12.15-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:7fbc8a7c410bb3ad5d595bb7118147dfbb6449d862cc1125cf8867cb337e8728"},
- {file = "aiohttp-3.12.15-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:74dad41b3458dbb0511e760fb355bb0b6689e0630de8a22b1b62a98777136e16"},
- {file = "aiohttp-3.12.15-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3b6f0af863cf17e6222b1735a756d664159e58855da99cfe965134a3ff63b0b0"},
- {file = "aiohttp-3.12.15-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b5b7fe4972d48a4da367043b8e023fb70a04d1490aa7d68800e465d1b97e493b"},
- {file = "aiohttp-3.12.15-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6443cca89553b7a5485331bc9bedb2342b08d073fa10b8c7d1c60579c4a7b9bd"},
- {file = "aiohttp-3.12.15-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6c5f40ec615e5264f44b4282ee27628cea221fcad52f27405b80abb346d9f3f8"},
- {file = "aiohttp-3.12.15-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:2abbb216a1d3a2fe86dbd2edce20cdc5e9ad0be6378455b05ec7f77361b3ab50"},
- {file = "aiohttp-3.12.15-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:db71ce547012a5420a39c1b744d485cfb823564d01d5d20805977f5ea1345676"},
- {file = "aiohttp-3.12.15-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:ced339d7c9b5030abad5854aa5413a77565e5b6e6248ff927d3e174baf3badf7"},
- {file = "aiohttp-3.12.15-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:7c7dd29c7b5bda137464dc9bfc738d7ceea46ff70309859ffde8c022e9b08ba7"},
- {file = "aiohttp-3.12.15-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:421da6fd326460517873274875c6c5a18ff225b40da2616083c5a34a7570b685"},
- {file = "aiohttp-3.12.15-cp311-cp311-win32.whl", hash = "sha256:4420cf9d179ec8dfe4be10e7d0fe47d6d606485512ea2265b0d8c5113372771b"},
- {file = "aiohttp-3.12.15-cp311-cp311-win_amd64.whl", hash = "sha256:edd533a07da85baa4b423ee8839e3e91681c7bfa19b04260a469ee94b778bf6d"},
- {file = "aiohttp-3.12.15-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:802d3868f5776e28f7bf69d349c26fc0efadb81676d0afa88ed00d98a26340b7"},
- {file = "aiohttp-3.12.15-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f2800614cd560287be05e33a679638e586a2d7401f4ddf99e304d98878c29444"},
- {file = "aiohttp-3.12.15-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8466151554b593909d30a0a125d638b4e5f3836e5aecde85b66b80ded1cb5b0d"},
- {file = "aiohttp-3.12.15-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2e5a495cb1be69dae4b08f35a6c4579c539e9b5706f606632102c0f855bcba7c"},
- {file = "aiohttp-3.12.15-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:6404dfc8cdde35c69aaa489bb3542fb86ef215fc70277c892be8af540e5e21c0"},
- {file = "aiohttp-3.12.15-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3ead1c00f8521a5c9070fcb88f02967b1d8a0544e6d85c253f6968b785e1a2ab"},
- {file = "aiohttp-3.12.15-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6990ef617f14450bc6b34941dba4f12d5613cbf4e33805932f853fbd1cf18bfb"},
- {file = "aiohttp-3.12.15-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd736ed420f4db2b8148b52b46b88ed038d0354255f9a73196b7bbce3ea97545"},
- {file = "aiohttp-3.12.15-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3c5092ce14361a73086b90c6efb3948ffa5be2f5b6fbcf52e8d8c8b8848bb97c"},
- {file = "aiohttp-3.12.15-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:aaa2234bb60c4dbf82893e934d8ee8dea30446f0647e024074237a56a08c01bd"},
- {file = "aiohttp-3.12.15-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:6d86a2fbdd14192e2f234a92d3b494dd4457e683ba07e5905a0b3ee25389ac9f"},
- {file = "aiohttp-3.12.15-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:a041e7e2612041a6ddf1c6a33b883be6a421247c7afd47e885969ee4cc58bd8d"},
- {file = "aiohttp-3.12.15-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:5015082477abeafad7203757ae44299a610e89ee82a1503e3d4184e6bafdd519"},
- {file = "aiohttp-3.12.15-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:56822ff5ddfd1b745534e658faba944012346184fbfe732e0d6134b744516eea"},
- {file = "aiohttp-3.12.15-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b2acbbfff69019d9014508c4ba0401822e8bae5a5fdc3b6814285b71231b60f3"},
- {file = "aiohttp-3.12.15-cp312-cp312-win32.whl", hash = "sha256:d849b0901b50f2185874b9a232f38e26b9b3d4810095a7572eacea939132d4e1"},
- {file = "aiohttp-3.12.15-cp312-cp312-win_amd64.whl", hash = "sha256:b390ef5f62bb508a9d67cb3bba9b8356e23b3996da7062f1a57ce1a79d2b3d34"},
- {file = "aiohttp-3.12.15-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:9f922ffd05034d439dde1c77a20461cf4a1b0831e6caa26151fe7aa8aaebc315"},
- {file = "aiohttp-3.12.15-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:2ee8a8ac39ce45f3e55663891d4b1d15598c157b4d494a4613e704c8b43112cd"},
- {file = "aiohttp-3.12.15-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3eae49032c29d356b94eee45a3f39fdf4b0814b397638c2f718e96cfadf4c4e4"},
- {file = "aiohttp-3.12.15-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b97752ff12cc12f46a9b20327104448042fce5c33a624f88c18f66f9368091c7"},
- {file = "aiohttp-3.12.15-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:894261472691d6fe76ebb7fcf2e5870a2ac284c7406ddc95823c8598a1390f0d"},
- {file = "aiohttp-3.12.15-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5fa5d9eb82ce98959fc1031c28198b431b4d9396894f385cb63f1e2f3f20ca6b"},
- {file = "aiohttp-3.12.15-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f0fa751efb11a541f57db59c1dd821bec09031e01452b2b6217319b3a1f34f3d"},
- {file = "aiohttp-3.12.15-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5346b93e62ab51ee2a9d68e8f73c7cf96ffb73568a23e683f931e52450e4148d"},
- {file = "aiohttp-3.12.15-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:049ec0360f939cd164ecbfd2873eaa432613d5e77d6b04535e3d1fbae5a9e645"},
- {file = "aiohttp-3.12.15-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:b52dcf013b57464b6d1e51b627adfd69a8053e84b7103a7cd49c030f9ca44461"},
- {file = "aiohttp-3.12.15-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:9b2af240143dd2765e0fb661fd0361a1b469cab235039ea57663cda087250ea9"},
- {file = "aiohttp-3.12.15-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ac77f709a2cde2cc71257ab2d8c74dd157c67a0558a0d2799d5d571b4c63d44d"},
- {file = "aiohttp-3.12.15-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:47f6b962246f0a774fbd3b6b7be25d59b06fdb2f164cf2513097998fc6a29693"},
- {file = "aiohttp-3.12.15-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:760fb7db442f284996e39cf9915a94492e1896baac44f06ae551974907922b64"},
- {file = "aiohttp-3.12.15-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ad702e57dc385cae679c39d318def49aef754455f237499d5b99bea4ef582e51"},
- {file = "aiohttp-3.12.15-cp313-cp313-win32.whl", hash = "sha256:f813c3e9032331024de2eb2e32a88d86afb69291fbc37a3a3ae81cc9917fb3d0"},
- {file = "aiohttp-3.12.15-cp313-cp313-win_amd64.whl", hash = "sha256:1a649001580bdb37c6fdb1bebbd7e3bc688e8ec2b5c6f52edbb664662b17dc84"},
- {file = "aiohttp-3.12.15-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:691d203c2bdf4f4637792efbbcdcd157ae11e55eaeb5e9c360c1206fb03d4d98"},
- {file = "aiohttp-3.12.15-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:8e995e1abc4ed2a454c731385bf4082be06f875822adc4c6d9eaadf96e20d406"},
- {file = "aiohttp-3.12.15-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:bd44d5936ab3193c617bfd6c9a7d8d1085a8dc8c3f44d5f1dcf554d17d04cf7d"},
- {file = "aiohttp-3.12.15-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:46749be6e89cd78d6068cdf7da51dbcfa4321147ab8e4116ee6678d9a056a0cf"},
- {file = "aiohttp-3.12.15-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0c643f4d75adea39e92c0f01b3fb83d57abdec8c9279b3078b68a3a52b3933b6"},
- {file = "aiohttp-3.12.15-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0a23918fedc05806966a2438489dcffccbdf83e921a1170773b6178d04ade142"},
- {file = "aiohttp-3.12.15-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:74bdd8c864b36c3673741023343565d95bfbd778ffe1eb4d412c135a28a8dc89"},
- {file = "aiohttp-3.12.15-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0a146708808c9b7a988a4af3821379e379e0f0e5e466ca31a73dbdd0325b0263"},
- {file = "aiohttp-3.12.15-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b7011a70b56facde58d6d26da4fec3280cc8e2a78c714c96b7a01a87930a9530"},
- {file = "aiohttp-3.12.15-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:3bdd6e17e16e1dbd3db74d7f989e8af29c4d2e025f9828e6ef45fbdee158ec75"},
- {file = "aiohttp-3.12.15-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:57d16590a351dfc914670bd72530fd78344b885a00b250e992faea565b7fdc05"},
- {file = "aiohttp-3.12.15-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:bc9a0f6569ff990e0bbd75506c8d8fe7214c8f6579cca32f0546e54372a3bb54"},
- {file = "aiohttp-3.12.15-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:536ad7234747a37e50e7b6794ea868833d5220b49c92806ae2d7e8a9d6b5de02"},
- {file = "aiohttp-3.12.15-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:f0adb4177fa748072546fb650d9bd7398caaf0e15b370ed3317280b13f4083b0"},
- {file = "aiohttp-3.12.15-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:14954a2988feae3987f1eb49c706bff39947605f4b6fa4027c1d75743723eb09"},
- {file = "aiohttp-3.12.15-cp39-cp39-win32.whl", hash = "sha256:b784d6ed757f27574dca1c336f968f4e81130b27595e458e69457e6878251f5d"},
- {file = "aiohttp-3.12.15-cp39-cp39-win_amd64.whl", hash = "sha256:86ceded4e78a992f835209e236617bffae649371c4a50d5e5a3987f237db84b8"},
- {file = "aiohttp-3.12.15.tar.gz", hash = "sha256:4fc61385e9c98d72fcdf47e6dd81833f47b2f77c114c29cd64a361be57a763a2"},
+ {file = "aiohttp-3.13.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d5a372fd5afd301b3a89582817fdcdb6c34124787c70dbcc616f259013e7eef7"},
+ {file = "aiohttp-3.13.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:147e422fd1223005c22b4fe080f5d93ced44460f5f9c105406b753612b587821"},
+ {file = "aiohttp-3.13.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:859bd3f2156e81dd01432f5849fc73e2243d4a487c4fd26609b1299534ee1845"},
+ {file = "aiohttp-3.13.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dca68018bf48c251ba17c72ed479f4dafe9dbd5a73707ad8d28a38d11f3d42af"},
+ {file = "aiohttp-3.13.3-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:fee0c6bc7db1de362252affec009707a17478a00ec69f797d23ca256e36d5940"},
+ {file = "aiohttp-3.13.3-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c048058117fd649334d81b4b526e94bde3ccaddb20463a815ced6ecbb7d11160"},
+ {file = "aiohttp-3.13.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:215a685b6fbbfcf71dfe96e3eba7a6f58f10da1dfdf4889c7dd856abe430dca7"},
+ {file = "aiohttp-3.13.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:de2c184bb1fe2cbd2cefba613e9db29a5ab559323f994b6737e370d3da0ac455"},
+ {file = "aiohttp-3.13.3-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:75ca857eba4e20ce9f546cd59c7007b33906a4cd48f2ff6ccf1ccfc3b646f279"},
+ {file = "aiohttp-3.13.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:81e97251d9298386c2b7dbeb490d3d1badbdc69107fb8c9299dd04eb39bddc0e"},
+ {file = "aiohttp-3.13.3-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:c0e2d366af265797506f0283487223146af57815b388623f0357ef7eac9b209d"},
+ {file = "aiohttp-3.13.3-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:4e239d501f73d6db1522599e14b9b321a7e3b1de66ce33d53a765d975e9f4808"},
+ {file = "aiohttp-3.13.3-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:0db318f7a6f065d84cb1e02662c526294450b314a02bd9e2a8e67f0d8564ce40"},
+ {file = "aiohttp-3.13.3-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:bfc1cc2fe31a6026a8a88e4ecfb98d7f6b1fec150cfd708adbfd1d2f42257c29"},
+ {file = "aiohttp-3.13.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:af71fff7bac6bb7508956696dce8f6eec2bbb045eceb40343944b1ae62b5ef11"},
+ {file = "aiohttp-3.13.3-cp310-cp310-win32.whl", hash = "sha256:37da61e244d1749798c151421602884db5270faf479cf0ef03af0ff68954c9dd"},
+ {file = "aiohttp-3.13.3-cp310-cp310-win_amd64.whl", hash = "sha256:7e63f210bc1b57ef699035f2b4b6d9ce096b5914414a49b0997c839b2bd2223c"},
+ {file = "aiohttp-3.13.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:5b6073099fb654e0a068ae678b10feff95c5cae95bbfcbfa7af669d361a8aa6b"},
+ {file = "aiohttp-3.13.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1cb93e166e6c28716c8c6aeb5f99dfb6d5ccf482d29fe9bf9a794110e6d0ab64"},
+ {file = "aiohttp-3.13.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:28e027cf2f6b641693a09f631759b4d9ce9165099d2b5d92af9bd4e197690eea"},
+ {file = "aiohttp-3.13.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3b61b7169ababd7802f9568ed96142616a9118dd2be0d1866e920e77ec8fa92a"},
+ {file = "aiohttp-3.13.3-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:80dd4c21b0f6237676449c6baaa1039abae86b91636b6c91a7f8e61c87f89540"},
+ {file = "aiohttp-3.13.3-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:65d2ccb7eabee90ce0503c17716fc77226be026dcc3e65cce859a30db715025b"},
+ {file = "aiohttp-3.13.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5b179331a481cb5529fca8b432d8d3c7001cb217513c94cd72d668d1248688a3"},
+ {file = "aiohttp-3.13.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d4c940f02f49483b18b079d1c27ab948721852b281f8b015c058100e9421dd1"},
+ {file = "aiohttp-3.13.3-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f9444f105664c4ce47a2a7171a2418bce5b7bae45fb610f4e2c36045d85911d3"},
+ {file = "aiohttp-3.13.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:694976222c711d1d00ba131904beb60534f93966562f64440d0c9d41b8cdb440"},
+ {file = "aiohttp-3.13.3-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:f33ed1a2bf1997a36661874b017f5c4b760f41266341af36febaf271d179f6d7"},
+ {file = "aiohttp-3.13.3-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:e636b3c5f61da31a92bf0d91da83e58fdfa96f178ba682f11d24f31944cdd28c"},
+ {file = "aiohttp-3.13.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:5d2d94f1f5fcbe40838ac51a6ab5704a6f9ea42e72ceda48de5e6b898521da51"},
+ {file = "aiohttp-3.13.3-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:2be0e9ccf23e8a94f6f0650ce06042cefc6ac703d0d7ab6c7a917289f2539ad4"},
+ {file = "aiohttp-3.13.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9af5e68ee47d6534d36791bbe9b646d2a7c7deb6fc24d7943628edfbb3581f29"},
+ {file = "aiohttp-3.13.3-cp311-cp311-win32.whl", hash = "sha256:a2212ad43c0833a873d0fb3c63fa1bacedd4cf6af2fee62bf4b739ceec3ab239"},
+ {file = "aiohttp-3.13.3-cp311-cp311-win_amd64.whl", hash = "sha256:642f752c3eb117b105acbd87e2c143de710987e09860d674e068c4c2c441034f"},
+ {file = "aiohttp-3.13.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:b903a4dfee7d347e2d87697d0713be59e0b87925be030c9178c5faa58ea58d5c"},
+ {file = "aiohttp-3.13.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a45530014d7a1e09f4a55f4f43097ba0fd155089372e105e4bff4ca76cb1b168"},
+ {file = "aiohttp-3.13.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:27234ef6d85c914f9efeb77ff616dbf4ad2380be0cda40b4db086ffc7ddd1b7d"},
+ {file = "aiohttp-3.13.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d32764c6c9aafb7fb55366a224756387cd50bfa720f32b88e0e6fa45b27dcf29"},
+ {file = "aiohttp-3.13.3-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:b1a6102b4d3ebc07dad44fbf07b45bb600300f15b552ddf1851b5390202ea2e3"},
+ {file = "aiohttp-3.13.3-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c014c7ea7fb775dd015b2d3137378b7be0249a448a1612268b5a90c2d81de04d"},
+ {file = "aiohttp-3.13.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2b8d8ddba8f95ba17582226f80e2de99c7a7948e66490ef8d947e272a93e9463"},
+ {file = "aiohttp-3.13.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9ae8dd55c8e6c4257eae3a20fd2c8f41edaea5992ed67156642493b8daf3cecc"},
+ {file = "aiohttp-3.13.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:01ad2529d4b5035578f5081606a465f3b814c542882804e2e8cda61adf5c71bf"},
+ {file = "aiohttp-3.13.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:bb4f7475e359992b580559e008c598091c45b5088f28614e855e42d39c2f1033"},
+ {file = "aiohttp-3.13.3-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:c19b90316ad3b24c69cd78d5c9b4f3aa4497643685901185b65166293d36a00f"},
+ {file = "aiohttp-3.13.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:96d604498a7c782cb15a51c406acaea70d8c027ee6b90c569baa6e7b93073679"},
+ {file = "aiohttp-3.13.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:084911a532763e9d3dd95adf78a78f4096cd5f58cdc18e6fdbc1b58417a45423"},
+ {file = "aiohttp-3.13.3-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:7a4a94eb787e606d0a09404b9c38c113d3b099d508021faa615d70a0131907ce"},
+ {file = "aiohttp-3.13.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:87797e645d9d8e222e04160ee32aa06bc5c163e8499f24db719e7852ec23093a"},
+ {file = "aiohttp-3.13.3-cp312-cp312-win32.whl", hash = "sha256:b04be762396457bef43f3597c991e192ee7da460a4953d7e647ee4b1c28e7046"},
+ {file = "aiohttp-3.13.3-cp312-cp312-win_amd64.whl", hash = "sha256:e3531d63d3bdfa7e3ac5e9b27b2dd7ec9df3206a98e0b3445fa906f233264c57"},
+ {file = "aiohttp-3.13.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:5dff64413671b0d3e7d5918ea490bdccb97a4ad29b3f311ed423200b2203e01c"},
+ {file = "aiohttp-3.13.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:87b9aab6d6ed88235aa2970294f496ff1a1f9adcd724d800e9b952395a80ffd9"},
+ {file = "aiohttp-3.13.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:425c126c0dc43861e22cb1c14ba4c8e45d09516d0a3ae0a3f7494b79f5f233a3"},
+ {file = "aiohttp-3.13.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7f9120f7093c2a32d9647abcaf21e6ad275b4fbec5b55969f978b1a97c7c86bf"},
+ {file = "aiohttp-3.13.3-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:697753042d57f4bf7122cab985bf15d0cef23c770864580f5af4f52023a56bd6"},
+ {file = "aiohttp-3.13.3-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6de499a1a44e7de70735d0b39f67c8f25eb3d91eb3103be99ca0fa882cdd987d"},
+ {file = "aiohttp-3.13.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:37239e9f9a7ea9ac5bf6b92b0260b01f8a22281996da609206a84df860bc1261"},
+ {file = "aiohttp-3.13.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f76c1e3fe7d7c8afad7ed193f89a292e1999608170dcc9751a7462a87dfd5bc0"},
+ {file = "aiohttp-3.13.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fc290605db2a917f6e81b0e1e0796469871f5af381ce15c604a3c5c7e51cb730"},
+ {file = "aiohttp-3.13.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4021b51936308aeea0367b8f006dc999ca02bc118a0cc78c303f50a2ff6afb91"},
+ {file = "aiohttp-3.13.3-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:49a03727c1bba9a97d3e93c9f93ca03a57300f484b6e935463099841261195d3"},
+ {file = "aiohttp-3.13.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:3d9908a48eb7416dc1f4524e69f1d32e5d90e3981e4e37eb0aa1cd18f9cfa2a4"},
+ {file = "aiohttp-3.13.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:2712039939ec963c237286113c68dbad80a82a4281543f3abf766d9d73228998"},
+ {file = "aiohttp-3.13.3-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:7bfdc049127717581866fa4708791220970ce291c23e28ccf3922c700740fdc0"},
+ {file = "aiohttp-3.13.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8057c98e0c8472d8846b9c79f56766bcc57e3e8ac7bfd510482332366c56c591"},
+ {file = "aiohttp-3.13.3-cp313-cp313-win32.whl", hash = "sha256:1449ceddcdbcf2e0446957863af03ebaaa03f94c090f945411b61269e2cb5daf"},
+ {file = "aiohttp-3.13.3-cp313-cp313-win_amd64.whl", hash = "sha256:693781c45a4033d31d4187d2436f5ac701e7bbfe5df40d917736108c1cc7436e"},
+ {file = "aiohttp-3.13.3-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:ea37047c6b367fd4bd632bff8077449b8fa034b69e812a18e0132a00fae6e808"},
+ {file = "aiohttp-3.13.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:6fc0e2337d1a4c3e6acafda6a78a39d4c14caea625124817420abceed36e2415"},
+ {file = "aiohttp-3.13.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c685f2d80bb67ca8c3837823ad76196b3694b0159d232206d1e461d3d434666f"},
+ {file = "aiohttp-3.13.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:48e377758516d262bde50c2584fc6c578af272559c409eecbdd2bae1601184d6"},
+ {file = "aiohttp-3.13.3-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:34749271508078b261c4abb1767d42b8d0c0cc9449c73a4df494777dc55f0687"},
+ {file = "aiohttp-3.13.3-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:82611aeec80eb144416956ec85b6ca45a64d76429c1ed46ae1b5f86c6e0c9a26"},
+ {file = "aiohttp-3.13.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2fff83cfc93f18f215896e3a190e8e5cb413ce01553901aca925176e7568963a"},
+ {file = "aiohttp-3.13.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bbe7d4cecacb439e2e2a8a1a7b935c25b812af7a5fd26503a66dadf428e79ec1"},
+ {file = "aiohttp-3.13.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b928f30fe49574253644b1ca44b1b8adbd903aa0da4b9054a6c20fc7f4092a25"},
+ {file = "aiohttp-3.13.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7b5e8fe4de30df199155baaf64f2fcd604f4c678ed20910db8e2c66dc4b11603"},
+ {file = "aiohttp-3.13.3-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:8542f41a62bcc58fc7f11cf7c90e0ec324ce44950003feb70640fc2a9092c32a"},
+ {file = "aiohttp-3.13.3-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:5e1d8c8b8f1d91cd08d8f4a3c2b067bfca6ec043d3ff36de0f3a715feeedf926"},
+ {file = "aiohttp-3.13.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:90455115e5da1c3c51ab619ac57f877da8fd6d73c05aacd125c5ae9819582aba"},
+ {file = "aiohttp-3.13.3-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:042e9e0bcb5fba81886c8b4fbb9a09d6b8a00245fd8d88e4d989c1f96c74164c"},
+ {file = "aiohttp-3.13.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2eb752b102b12a76ca02dff751a801f028b4ffbbc478840b473597fc91a9ed43"},
+ {file = "aiohttp-3.13.3-cp314-cp314-win32.whl", hash = "sha256:b556c85915d8efaed322bf1bdae9486aa0f3f764195a0fb6ee962e5c71ef5ce1"},
+ {file = "aiohttp-3.13.3-cp314-cp314-win_amd64.whl", hash = "sha256:9bf9f7a65e7aa20dd764151fb3d616c81088f91f8df39c3893a536e279b4b984"},
+ {file = "aiohttp-3.13.3-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:05861afbbec40650d8a07ea324367cb93e9e8cc7762e04dd4405df99fa65159c"},
+ {file = "aiohttp-3.13.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:2fc82186fadc4a8316768d61f3722c230e2c1dcab4200d52d2ebdf2482e47592"},
+ {file = "aiohttp-3.13.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:0add0900ff220d1d5c5ebbf99ed88b0c1bbf87aa7e4262300ed1376a6b13414f"},
+ {file = "aiohttp-3.13.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:568f416a4072fbfae453dcf9a99194bbb8bdeab718e08ee13dfa2ba0e4bebf29"},
+ {file = "aiohttp-3.13.3-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:add1da70de90a2569c5e15249ff76a631ccacfe198375eead4aadf3b8dc849dc"},
+ {file = "aiohttp-3.13.3-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:10b47b7ba335d2e9b1239fa571131a87e2d8ec96b333e68b2a305e7a98b0bae2"},
+ {file = "aiohttp-3.13.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3dd4dce1c718e38081c8f35f323209d4c1df7d4db4bab1b5c88a6b4d12b74587"},
+ {file = "aiohttp-3.13.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:34bac00a67a812570d4a460447e1e9e06fae622946955f939051e7cc895cfab8"},
+ {file = "aiohttp-3.13.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a19884d2ee70b06d9204b2727a7b9f983d0c684c650254679e716b0b77920632"},
+ {file = "aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5f8ca7f2bb6ba8348a3614c7918cc4bb73268c5ac2a207576b7afea19d3d9f64"},
+ {file = "aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:b0d95340658b9d2f11d9697f59b3814a9d3bb4b7a7c20b131df4bcef464037c0"},
+ {file = "aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:a1e53262fd202e4b40b70c3aff944a8155059beedc8a89bba9dc1f9ef06a1b56"},
+ {file = "aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:d60ac9663f44168038586cab2157e122e46bdef09e9368b37f2d82d354c23f72"},
+ {file = "aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:90751b8eed69435bac9ff4e3d2f6b3af1f57e37ecb0fbeee59c0174c9e2d41df"},
+ {file = "aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:fc353029f176fd2b3ec6cfc71be166aba1936fe5d73dd1992ce289ca6647a9aa"},
+ {file = "aiohttp-3.13.3-cp314-cp314t-win32.whl", hash = "sha256:2e41b18a58da1e474a057b3d35248d8320029f61d70a37629535b16a0c8f3767"},
+ {file = "aiohttp-3.13.3-cp314-cp314t-win_amd64.whl", hash = "sha256:44531a36aa2264a1860089ffd4dce7baf875ee5a6079d5fb42e261c704ef7344"},
+ {file = "aiohttp-3.13.3-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:31a83ea4aead760dfcb6962efb1d861db48c34379f2ff72db9ddddd4cda9ea2e"},
+ {file = "aiohttp-3.13.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:988a8c5e317544fdf0d39871559e67b6341065b87fceac641108c2096d5506b7"},
+ {file = "aiohttp-3.13.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:9b174f267b5cfb9a7dba9ee6859cecd234e9a681841eb85068059bc867fb8f02"},
+ {file = "aiohttp-3.13.3-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:947c26539750deeaee933b000fb6517cc770bbd064bad6033f1cff4803881e43"},
+ {file = "aiohttp-3.13.3-cp39-cp39-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:9ebf57d09e131f5323464bd347135a88622d1c0976e88ce15b670e7ad57e4bd6"},
+ {file = "aiohttp-3.13.3-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4ae5b5a0e1926e504c81c5b84353e7a5516d8778fbbff00429fe7b05bb25cbce"},
+ {file = "aiohttp-3.13.3-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2ba0eea45eb5cc3172dbfc497c066f19c41bac70963ea1a67d51fc92e4cf9a80"},
+ {file = "aiohttp-3.13.3-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bae5c2ed2eae26cc382020edad80d01f36cb8e746da40b292e68fec40421dc6a"},
+ {file = "aiohttp-3.13.3-cp39-cp39-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8a60e60746623925eab7d25823329941aee7242d559baa119ca2b253c88a7bd6"},
+ {file = "aiohttp-3.13.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:e50a2e1404f063427c9d027378472316201a2290959a295169bcf25992d04558"},
+ {file = "aiohttp-3.13.3-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:9a9dc347e5a3dc7dfdbc1f82da0ef29e388ddb2ed281bfce9dd8248a313e62b7"},
+ {file = "aiohttp-3.13.3-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:b46020d11d23fe16551466c77823df9cc2f2c1e63cc965daf67fa5eec6ca1877"},
+ {file = "aiohttp-3.13.3-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:69c56fbc1993fa17043e24a546959c0178fe2b5782405ad4559e6c13975c15e3"},
+ {file = "aiohttp-3.13.3-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:b99281b0704c103d4e11e72a76f1b543d4946fea7dd10767e7e1b5f00d4e5704"},
+ {file = "aiohttp-3.13.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:40c5e40ecc29ba010656c18052b877a1c28f84344825efa106705e835c28530f"},
+ {file = "aiohttp-3.13.3-cp39-cp39-win32.whl", hash = "sha256:56339a36b9f1fc708260c76c87e593e2afb30d26de9ae1eb445b5e051b98a7a1"},
+ {file = "aiohttp-3.13.3-cp39-cp39-win_amd64.whl", hash = "sha256:c6b8568a3bb5819a0ad087f16d40e5a3fb6099f39ea1d5625a3edc1e923fc538"},
+ {file = "aiohttp-3.13.3.tar.gz", hash = "sha256:a949eee43d3782f2daae4f4a2819b2cb9b0c5d3b7f7a927067cc84dafdbb9f88"},
]
[package.dependencies]
@@ -142,7 +176,7 @@ propcache = ">=0.2.0"
yarl = ">=1.17.0,<2.0"
[package.extras]
-speedups = ["Brotli ; platform_python_implementation == \"CPython\"", "aiodns (>=3.3.0)", "brotlicffi ; platform_python_implementation != \"CPython\""]
+speedups = ["Brotli (>=1.2) ; platform_python_implementation == \"CPython\"", "aiodns (>=3.3.0)", "backports.zstd ; platform_python_implementation == \"CPython\" and python_version < \"3.14\"", "brotlicffi (>=1.2) ; platform_python_implementation != \"CPython\""]
[[package]]
name = "aiosignal"
@@ -813,14 +847,14 @@ tests-mypy = ["mypy (>=1.11.1) ; platform_python_implementation == \"CPython\" a
[[package]]
name = "authlib"
-version = "1.6.5"
+version = "1.6.6"
description = "The ultimate Python library in building OAuth and OpenID Connect servers and clients."
optional = false
python-versions = ">=3.9"
groups = ["dev"]
files = [
- {file = "authlib-1.6.5-py2.py3-none-any.whl", hash = "sha256:3e0e0507807f842b02175507bdee8957a1d5707fd4afb17c32fb43fee90b6e3a"},
- {file = "authlib-1.6.5.tar.gz", hash = "sha256:6aaf9c79b7cc96c900f0b284061691c5d4e61221640a948fe690b556a6d6d10b"},
+ {file = "authlib-1.6.6-py2.py3-none-any.whl", hash = "sha256:7d9e9bc535c13974313a87f53e8430eb6ea3d1cf6ae4f6efcd793f2e949143fd"},
+ {file = "authlib-1.6.6.tar.gz", hash = "sha256:45770e8e056d0f283451d9996fbb59b70d45722b45d854d58f32878d0a40c38e"},
]
[package.dependencies]
@@ -1554,84 +1588,101 @@ files = [
[[package]]
name = "cffi"
-version = "1.17.1"
+version = "2.0.0"
description = "Foreign Function Interface for Python calling C code."
optional = false
-python-versions = ">=3.8"
+python-versions = ">=3.9"
groups = ["main", "dev"]
+markers = "platform_python_implementation != \"PyPy\""
files = [
- {file = "cffi-1.17.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:df8b1c11f177bc2313ec4b2d46baec87a5f3e71fc8b45dab2ee7cae86d9aba14"},
- {file = "cffi-1.17.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8f2cdc858323644ab277e9bb925ad72ae0e67f69e804f4898c070998d50b1a67"},
- {file = "cffi-1.17.1-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:edae79245293e15384b51f88b00613ba9f7198016a5948b5dddf4917d4d26382"},
- {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:45398b671ac6d70e67da8e4224a065cec6a93541bb7aebe1b198a61b58c7b702"},
- {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ad9413ccdeda48c5afdae7e4fa2192157e991ff761e7ab8fdd8926f40b160cc3"},
- {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5da5719280082ac6bd9aa7becb3938dc9f9cbd57fac7d2871717b1feb0902ab6"},
- {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2bb1a08b8008b281856e5971307cc386a8e9c5b625ac297e853d36da6efe9c17"},
- {file = "cffi-1.17.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:045d61c734659cc045141be4bae381a41d89b741f795af1dd018bfb532fd0df8"},
- {file = "cffi-1.17.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:6883e737d7d9e4899a8a695e00ec36bd4e5e4f18fabe0aca0efe0a4b44cdb13e"},
- {file = "cffi-1.17.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:6b8b4a92e1c65048ff98cfe1f735ef8f1ceb72e3d5f0c25fdb12087a23da22be"},
- {file = "cffi-1.17.1-cp310-cp310-win32.whl", hash = "sha256:c9c3d058ebabb74db66e431095118094d06abf53284d9c81f27300d0e0d8bc7c"},
- {file = "cffi-1.17.1-cp310-cp310-win_amd64.whl", hash = "sha256:0f048dcf80db46f0098ccac01132761580d28e28bc0f78ae0d58048063317e15"},
- {file = "cffi-1.17.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a45e3c6913c5b87b3ff120dcdc03f6131fa0065027d0ed7ee6190736a74cd401"},
- {file = "cffi-1.17.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:30c5e0cb5ae493c04c8b42916e52ca38079f1b235c2f8ae5f4527b963c401caf"},
- {file = "cffi-1.17.1-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f75c7ab1f9e4aca5414ed4d8e5c0e303a34f4421f8a0d47a4d019ceff0ab6af4"},
- {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a1ed2dd2972641495a3ec98445e09766f077aee98a1c896dcb4ad0d303628e41"},
- {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:46bf43160c1a35f7ec506d254e5c890f3c03648a4dbac12d624e4490a7046cd1"},
- {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a24ed04c8ffd54b0729c07cee15a81d964e6fee0e3d4d342a27b020d22959dc6"},
- {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:610faea79c43e44c71e1ec53a554553fa22321b65fae24889706c0a84d4ad86d"},
- {file = "cffi-1.17.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:a9b15d491f3ad5d692e11f6b71f7857e7835eb677955c00cc0aefcd0669adaf6"},
- {file = "cffi-1.17.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:de2ea4b5833625383e464549fec1bc395c1bdeeb5f25c4a3a82b5a8c756ec22f"},
- {file = "cffi-1.17.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:fc48c783f9c87e60831201f2cce7f3b2e4846bf4d8728eabe54d60700b318a0b"},
- {file = "cffi-1.17.1-cp311-cp311-win32.whl", hash = "sha256:85a950a4ac9c359340d5963966e3e0a94a676bd6245a4b55bc43949eee26a655"},
- {file = "cffi-1.17.1-cp311-cp311-win_amd64.whl", hash = "sha256:caaf0640ef5f5517f49bc275eca1406b0ffa6aa184892812030f04c2abf589a0"},
- {file = "cffi-1.17.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:805b4371bf7197c329fcb3ead37e710d1bca9da5d583f5073b799d5c5bd1eee4"},
- {file = "cffi-1.17.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:733e99bc2df47476e3848417c5a4540522f234dfd4ef3ab7fafdf555b082ec0c"},
- {file = "cffi-1.17.1-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1257bdabf294dceb59f5e70c64a3e2f462c30c7ad68092d01bbbfb1c16b1ba36"},
- {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da95af8214998d77a98cc14e3a3bd00aa191526343078b530ceb0bd710fb48a5"},
- {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d63afe322132c194cf832bfec0dc69a99fb9bb6bbd550f161a49e9e855cc78ff"},
- {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f79fc4fc25f1c8698ff97788206bb3c2598949bfe0fef03d299eb1b5356ada99"},
- {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b62ce867176a75d03a665bad002af8e6d54644fad99a3c70905c543130e39d93"},
- {file = "cffi-1.17.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:386c8bf53c502fff58903061338ce4f4950cbdcb23e2902d86c0f722b786bbe3"},
- {file = "cffi-1.17.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4ceb10419a9adf4460ea14cfd6bc43d08701f0835e979bf821052f1805850fe8"},
- {file = "cffi-1.17.1-cp312-cp312-win32.whl", hash = "sha256:a08d7e755f8ed21095a310a693525137cfe756ce62d066e53f502a83dc550f65"},
- {file = "cffi-1.17.1-cp312-cp312-win_amd64.whl", hash = "sha256:51392eae71afec0d0c8fb1a53b204dbb3bcabcb3c9b807eedf3e1e6ccf2de903"},
- {file = "cffi-1.17.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f3a2b4222ce6b60e2e8b337bb9596923045681d71e5a082783484d845390938e"},
- {file = "cffi-1.17.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0984a4925a435b1da406122d4d7968dd861c1385afe3b45ba82b750f229811e2"},
- {file = "cffi-1.17.1-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d01b12eeeb4427d3110de311e1774046ad344f5b1a7403101878976ecd7a10f3"},
- {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:706510fe141c86a69c8ddc029c7910003a17353970cff3b904ff0686a5927683"},
- {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de55b766c7aa2e2a3092c51e0483d700341182f08e67c63630d5b6f200bb28e5"},
- {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c59d6e989d07460165cc5ad3c61f9fd8f1b4796eacbd81cee78957842b834af4"},
- {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd398dbc6773384a17fe0d3e7eeb8d1a21c2200473ee6806bb5e6a8e62bb73dd"},
- {file = "cffi-1.17.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:3edc8d958eb099c634dace3c7e16560ae474aa3803a5df240542b305d14e14ed"},
- {file = "cffi-1.17.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:72e72408cad3d5419375fc87d289076ee319835bdfa2caad331e377589aebba9"},
- {file = "cffi-1.17.1-cp313-cp313-win32.whl", hash = "sha256:e03eab0a8677fa80d646b5ddece1cbeaf556c313dcfac435ba11f107ba117b5d"},
- {file = "cffi-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:f6a16c31041f09ead72d69f583767292f750d24913dadacf5756b966aacb3f1a"},
- {file = "cffi-1.17.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:636062ea65bd0195bc012fea9321aca499c0504409f413dc88af450b57ffd03b"},
- {file = "cffi-1.17.1-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c7eac2ef9b63c79431bc4b25f1cd649d7f061a28808cbc6c47b534bd789ef964"},
- {file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e221cf152cff04059d011ee126477f0d9588303eb57e88923578ace7baad17f9"},
- {file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:31000ec67d4221a71bd3f67df918b1f88f676f1c3b535a7eb473255fdc0b83fc"},
- {file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6f17be4345073b0a7b8ea599688f692ac3ef23ce28e5df79c04de519dbc4912c"},
- {file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0e2b1fac190ae3ebfe37b979cc1ce69c81f4e4fe5746bb401dca63a9062cdaf1"},
- {file = "cffi-1.17.1-cp38-cp38-win32.whl", hash = "sha256:7596d6620d3fa590f677e9ee430df2958d2d6d6de2feeae5b20e82c00b76fbf8"},
- {file = "cffi-1.17.1-cp38-cp38-win_amd64.whl", hash = "sha256:78122be759c3f8a014ce010908ae03364d00a1f81ab5c7f4a7a5120607ea56e1"},
- {file = "cffi-1.17.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b2ab587605f4ba0bf81dc0cb08a41bd1c0a5906bd59243d56bad7668a6fc6c16"},
- {file = "cffi-1.17.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:28b16024becceed8c6dfbc75629e27788d8a3f9030691a1dbf9821a128b22c36"},
- {file = "cffi-1.17.1-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1d599671f396c4723d016dbddb72fe8e0397082b0a77a4fab8028923bec050e8"},
- {file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca74b8dbe6e8e8263c0ffd60277de77dcee6c837a3d0881d8c1ead7268c9e576"},
- {file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f7f5baafcc48261359e14bcd6d9bff6d4b28d9103847c9e136694cb0501aef87"},
- {file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:98e3969bcff97cae1b2def8ba499ea3d6f31ddfdb7635374834cf89a1a08ecf0"},
- {file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cdf5ce3acdfd1661132f2a9c19cac174758dc2352bfe37d98aa7512c6b7178b3"},
- {file = "cffi-1.17.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:9755e4345d1ec879e3849e62222a18c7174d65a6a92d5b346b1863912168b595"},
- {file = "cffi-1.17.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:f1e22e8c4419538cb197e4dd60acc919d7696e5ef98ee4da4e01d3f8cfa4cc5a"},
- {file = "cffi-1.17.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:c03e868a0b3bc35839ba98e74211ed2b05d2119be4e8a0f224fba9384f1fe02e"},
- {file = "cffi-1.17.1-cp39-cp39-win32.whl", hash = "sha256:e31ae45bc2e29f6b2abd0de1cc3b9d5205aa847cafaecb8af1476a609a2f6eb7"},
- {file = "cffi-1.17.1-cp39-cp39-win_amd64.whl", hash = "sha256:d016c76bdd850f3c626af19b0542c9677ba156e4ee4fccfdd7848803533ef662"},
- {file = "cffi-1.17.1.tar.gz", hash = "sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824"},
+ {file = "cffi-2.0.0-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:0cf2d91ecc3fcc0625c2c530fe004f82c110405f101548512cce44322fa8ac44"},
+ {file = "cffi-2.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f73b96c41e3b2adedc34a7356e64c8eb96e03a3782b535e043a986276ce12a49"},
+ {file = "cffi-2.0.0-cp310-cp310-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:53f77cbe57044e88bbd5ed26ac1d0514d2acf0591dd6bb02a3ae37f76811b80c"},
+ {file = "cffi-2.0.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3e837e369566884707ddaf85fc1744b47575005c0a229de3327f8f9a20f4efeb"},
+ {file = "cffi-2.0.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:5eda85d6d1879e692d546a078b44251cdd08dd1cfb98dfb77b670c97cee49ea0"},
+ {file = "cffi-2.0.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9332088d75dc3241c702d852d4671613136d90fa6881da7d770a483fd05248b4"},
+ {file = "cffi-2.0.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fc7de24befaeae77ba923797c7c87834c73648a05a4bde34b3b7e5588973a453"},
+ {file = "cffi-2.0.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:cf364028c016c03078a23b503f02058f1814320a56ad535686f90565636a9495"},
+ {file = "cffi-2.0.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e11e82b744887154b182fd3e7e8512418446501191994dbf9c9fc1f32cc8efd5"},
+ {file = "cffi-2.0.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8ea985900c5c95ce9db1745f7933eeef5d314f0565b27625d9a10ec9881e1bfb"},
+ {file = "cffi-2.0.0-cp310-cp310-win32.whl", hash = "sha256:1f72fb8906754ac8a2cc3f9f5aaa298070652a0ffae577e0ea9bd480dc3c931a"},
+ {file = "cffi-2.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:b18a3ed7d5b3bd8d9ef7a8cb226502c6bf8308df1525e1cc676c3680e7176739"},
+ {file = "cffi-2.0.0-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:b4c854ef3adc177950a8dfc81a86f5115d2abd545751a304c5bcf2c2c7283cfe"},
+ {file = "cffi-2.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2de9a304e27f7596cd03d16f1b7c72219bd944e99cc52b84d0145aefb07cbd3c"},
+ {file = "cffi-2.0.0-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:baf5215e0ab74c16e2dd324e8ec067ef59e41125d3eade2b863d294fd5035c92"},
+ {file = "cffi-2.0.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:730cacb21e1bdff3ce90babf007d0a0917cc3e6492f336c2f0134101e0944f93"},
+ {file = "cffi-2.0.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:6824f87845e3396029f3820c206e459ccc91760e8fa24422f8b0c3d1731cbec5"},
+ {file = "cffi-2.0.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9de40a7b0323d889cf8d23d1ef214f565ab154443c42737dfe52ff82cf857664"},
+ {file = "cffi-2.0.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8941aaadaf67246224cee8c3803777eed332a19d909b47e29c9842ef1e79ac26"},
+ {file = "cffi-2.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a05d0c237b3349096d3981b727493e22147f934b20f6f125a3eba8f994bec4a9"},
+ {file = "cffi-2.0.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:94698a9c5f91f9d138526b48fe26a199609544591f859c870d477351dc7b2414"},
+ {file = "cffi-2.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:5fed36fccc0612a53f1d4d9a816b50a36702c28a2aa880cb8a122b3466638743"},
+ {file = "cffi-2.0.0-cp311-cp311-win32.whl", hash = "sha256:c649e3a33450ec82378822b3dad03cc228b8f5963c0c12fc3b1e0ab940f768a5"},
+ {file = "cffi-2.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:66f011380d0e49ed280c789fbd08ff0d40968ee7b665575489afa95c98196ab5"},
+ {file = "cffi-2.0.0-cp311-cp311-win_arm64.whl", hash = "sha256:c6638687455baf640e37344fe26d37c404db8b80d037c3d29f58fe8d1c3b194d"},
+ {file = "cffi-2.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d02d6655b0e54f54c4ef0b94eb6be0607b70853c45ce98bd278dc7de718be5d"},
+ {file = "cffi-2.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8eca2a813c1cb7ad4fb74d368c2ffbbb4789d377ee5bb8df98373c2cc0dee76c"},
+ {file = "cffi-2.0.0-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:21d1152871b019407d8ac3985f6775c079416c282e431a4da6afe7aefd2bccbe"},
+ {file = "cffi-2.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b21e08af67b8a103c71a250401c78d5e0893beff75e28c53c98f4de42f774062"},
+ {file = "cffi-2.0.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:1e3a615586f05fc4065a8b22b8152f0c1b00cdbc60596d187c2a74f9e3036e4e"},
+ {file = "cffi-2.0.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:81afed14892743bbe14dacb9e36d9e0e504cd204e0b165062c488942b9718037"},
+ {file = "cffi-2.0.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3e17ed538242334bf70832644a32a7aae3d83b57567f9fd60a26257e992b79ba"},
+ {file = "cffi-2.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3925dd22fa2b7699ed2617149842d2e6adde22b262fcbfada50e3d195e4b3a94"},
+ {file = "cffi-2.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2c8f814d84194c9ea681642fd164267891702542f028a15fc97d4674b6206187"},
+ {file = "cffi-2.0.0-cp312-cp312-win32.whl", hash = "sha256:da902562c3e9c550df360bfa53c035b2f241fed6d9aef119048073680ace4a18"},
+ {file = "cffi-2.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:da68248800ad6320861f129cd9c1bf96ca849a2771a59e0344e88681905916f5"},
+ {file = "cffi-2.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:4671d9dd5ec934cb9a73e7ee9676f9362aba54f7f34910956b84d727b0d73fb6"},
+ {file = "cffi-2.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:00bdf7acc5f795150faa6957054fbbca2439db2f775ce831222b66f192f03beb"},
+ {file = "cffi-2.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:45d5e886156860dc35862657e1494b9bae8dfa63bf56796f2fb56e1679fc0bca"},
+ {file = "cffi-2.0.0-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:07b271772c100085dd28b74fa0cd81c8fb1a3ba18b21e03d7c27f3436a10606b"},
+ {file = "cffi-2.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d48a880098c96020b02d5a1f7d9251308510ce8858940e6fa99ece33f610838b"},
+ {file = "cffi-2.0.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f93fd8e5c8c0a4aa1f424d6173f14a892044054871c771f8566e4008eaa359d2"},
+ {file = "cffi-2.0.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:dd4f05f54a52fb558f1ba9f528228066954fee3ebe629fc1660d874d040ae5a3"},
+ {file = "cffi-2.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c8d3b5532fc71b7a77c09192b4a5a200ea992702734a2e9279a37f2478236f26"},
+ {file = "cffi-2.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d9b29c1f0ae438d5ee9acb31cadee00a58c46cc9c0b2f9038c6b0b3470877a8c"},
+ {file = "cffi-2.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6d50360be4546678fc1b79ffe7a66265e28667840010348dd69a314145807a1b"},
+ {file = "cffi-2.0.0-cp313-cp313-win32.whl", hash = "sha256:74a03b9698e198d47562765773b4a8309919089150a0bb17d829ad7b44b60d27"},
+ {file = "cffi-2.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:19f705ada2530c1167abacb171925dd886168931e0a7b78f5bffcae5c6b5be75"},
+ {file = "cffi-2.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:256f80b80ca3853f90c21b23ee78cd008713787b1b1e93eae9f3d6a7134abd91"},
+ {file = "cffi-2.0.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fc33c5141b55ed366cfaad382df24fe7dcbc686de5be719b207bb248e3053dc5"},
+ {file = "cffi-2.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c654de545946e0db659b3400168c9ad31b5d29593291482c43e3564effbcee13"},
+ {file = "cffi-2.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:24b6f81f1983e6df8db3adc38562c83f7d4a0c36162885ec7f7b77c7dcbec97b"},
+ {file = "cffi-2.0.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:12873ca6cb9b0f0d3a0da705d6086fe911591737a59f28b7936bdfed27c0d47c"},
+ {file = "cffi-2.0.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:d9b97165e8aed9272a6bb17c01e3cc5871a594a446ebedc996e2397a1c1ea8ef"},
+ {file = "cffi-2.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:afb8db5439b81cf9c9d0c80404b60c3cc9c3add93e114dcae767f1477cb53775"},
+ {file = "cffi-2.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:737fe7d37e1a1bffe70bd5754ea763a62a066dc5913ca57e957824b72a85e205"},
+ {file = "cffi-2.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:38100abb9d1b1435bc4cc340bb4489635dc2f0da7456590877030c9b3d40b0c1"},
+ {file = "cffi-2.0.0-cp314-cp314-win32.whl", hash = "sha256:087067fa8953339c723661eda6b54bc98c5625757ea62e95eb4898ad5e776e9f"},
+ {file = "cffi-2.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:203a48d1fb583fc7d78a4c6655692963b860a417c0528492a6bc21f1aaefab25"},
+ {file = "cffi-2.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:dbd5c7a25a7cb98f5ca55d258b103a2054f859a46ae11aaf23134f9cc0d356ad"},
+ {file = "cffi-2.0.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:9a67fc9e8eb39039280526379fb3a70023d77caec1852002b4da7e8b270c4dd9"},
+ {file = "cffi-2.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7a66c7204d8869299919db4d5069a82f1561581af12b11b3c9f48c584eb8743d"},
+ {file = "cffi-2.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7cc09976e8b56f8cebd752f7113ad07752461f48a58cbba644139015ac24954c"},
+ {file = "cffi-2.0.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:92b68146a71df78564e4ef48af17551a5ddd142e5190cdf2c5624d0c3ff5b2e8"},
+ {file = "cffi-2.0.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b1e74d11748e7e98e2f426ab176d4ed720a64412b6a15054378afdb71e0f37dc"},
+ {file = "cffi-2.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:28a3a209b96630bca57cce802da70c266eb08c6e97e5afd61a75611ee6c64592"},
+ {file = "cffi-2.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7553fb2090d71822f02c629afe6042c299edf91ba1bf94951165613553984512"},
+ {file = "cffi-2.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6c6c373cfc5c83a975506110d17457138c8c63016b563cc9ed6e056a82f13ce4"},
+ {file = "cffi-2.0.0-cp314-cp314t-win32.whl", hash = "sha256:1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e"},
+ {file = "cffi-2.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6"},
+ {file = "cffi-2.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9"},
+ {file = "cffi-2.0.0-cp39-cp39-macosx_10_13_x86_64.whl", hash = "sha256:fe562eb1a64e67dd297ccc4f5addea2501664954f2692b69a76449ec7913ecbf"},
+ {file = "cffi-2.0.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:de8dad4425a6ca6e4e5e297b27b5c824ecc7581910bf9aee86cb6835e6812aa7"},
+ {file = "cffi-2.0.0-cp39-cp39-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:4647afc2f90d1ddd33441e5b0e85b16b12ddec4fca55f0d9671fef036ecca27c"},
+ {file = "cffi-2.0.0-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3f4d46d8b35698056ec29bca21546e1551a205058ae1a181d871e278b0b28165"},
+ {file = "cffi-2.0.0-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:e6e73b9e02893c764e7e8d5bb5ce277f1a009cd5243f8228f75f842bf937c534"},
+ {file = "cffi-2.0.0-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:cb527a79772e5ef98fb1d700678fe031e353e765d1ca2d409c92263c6d43e09f"},
+ {file = "cffi-2.0.0-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:61d028e90346df14fedc3d1e5441df818d095f3b87d286825dfcbd6459b7ef63"},
+ {file = "cffi-2.0.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:0f6084a0ea23d05d20c3edcda20c3d006f9b6f3fefeac38f59262e10cef47ee2"},
+ {file = "cffi-2.0.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:1cd13c99ce269b3ed80b417dcd591415d3372bcac067009b6e0f59c7d4015e65"},
+ {file = "cffi-2.0.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:89472c9762729b5ae1ad974b777416bfda4ac5642423fa93bd57a09204712322"},
+ {file = "cffi-2.0.0-cp39-cp39-win32.whl", hash = "sha256:2081580ebb843f759b9f617314a24ed5738c51d2aee65d31e02f6f7a2b97707a"},
+ {file = "cffi-2.0.0-cp39-cp39-win_amd64.whl", hash = "sha256:b882b3df248017dba09d6b16defe9b5c407fe32fc7c65a9c69798e6175601be9"},
+ {file = "cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529"},
]
-markers = {dev = "platform_python_implementation != \"PyPy\""}
[package.dependencies]
-pycparser = "*"
+pycparser = {version = "*", markers = "implementation_name != \"PyPy\""}
[[package]]
name = "charset-normalizer"
@@ -2257,14 +2308,14 @@ with-social = ["django-allauth[socialaccount] (>=64.0.0)"]
[[package]]
name = "django"
-version = "5.1.14"
+version = "5.1.15"
description = "A high-level Python web framework that encourages rapid development and clean, pragmatic design."
optional = false
python-versions = ">=3.10"
groups = ["main", "dev"]
files = [
- {file = "django-5.1.14-py3-none-any.whl", hash = "sha256:2a4b9c20404fd1bf50aaaa5542a19d860594cba1354f688f642feb271b91df27"},
- {file = "django-5.1.14.tar.gz", hash = "sha256:b98409fb31fdd6e8c3a6ba2eef3415cc5c0020057b43b21ba7af6eff5f014831"},
+ {file = "django-5.1.15-py3-none-any.whl", hash = "sha256:117871e58d6eda37f09870b7d73a3d66567b03aecd515b386b1751177c413432"},
+ {file = "django-5.1.15.tar.gz", hash = "sha256:46a356b5ff867bece73fc6365e081f21c569973403ee7e9b9a0316f27d0eb947"},
]
[package.dependencies]
@@ -2816,83 +2867,75 @@ dotenv = ["python-dotenv"]
[[package]]
name = "fonttools"
-version = "4.60.1"
+version = "4.61.1"
description = "Tools to manipulate font files"
optional = false
-python-versions = ">=3.9"
+python-versions = ">=3.10"
groups = ["main"]
files = [
- {file = "fonttools-4.60.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:9a52f254ce051e196b8fe2af4634c2d2f02c981756c6464dc192f1b6050b4e28"},
- {file = "fonttools-4.60.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c7420a2696a44650120cdd269a5d2e56a477e2bfa9d95e86229059beb1c19e15"},
- {file = "fonttools-4.60.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee0c0b3b35b34f782afc673d503167157094a16f442ace7c6c5e0ca80b08f50c"},
- {file = "fonttools-4.60.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:282dafa55f9659e8999110bd8ed422ebe1c8aecd0dc396550b038e6c9a08b8ea"},
- {file = "fonttools-4.60.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:4ba4bd646e86de16160f0fb72e31c3b9b7d0721c3e5b26b9fa2fc931dfdb2652"},
- {file = "fonttools-4.60.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:0b0835ed15dd5b40d726bb61c846a688f5b4ce2208ec68779bc81860adb5851a"},
- {file = "fonttools-4.60.1-cp310-cp310-win32.whl", hash = "sha256:1525796c3ffe27bb6268ed2a1bb0dcf214d561dfaf04728abf01489eb5339dce"},
- {file = "fonttools-4.60.1-cp310-cp310-win_amd64.whl", hash = "sha256:268ecda8ca6cb5c4f044b1fb9b3b376e8cd1b361cef275082429dc4174907038"},
- {file = "fonttools-4.60.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:7b4c32e232a71f63a5d00259ca3d88345ce2a43295bb049d21061f338124246f"},
- {file = "fonttools-4.60.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3630e86c484263eaac71d117085d509cbcf7b18f677906824e4bace598fb70d2"},
- {file = "fonttools-4.60.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5c1015318e4fec75dd4943ad5f6a206d9727adf97410d58b7e32ab644a807914"},
- {file = "fonttools-4.60.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:e6c58beb17380f7c2ea181ea11e7db8c0ceb474c9dd45f48e71e2cb577d146a1"},
- {file = "fonttools-4.60.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:ec3681a0cb34c255d76dd9d865a55f260164adb9fa02628415cdc2d43ee2c05d"},
- {file = "fonttools-4.60.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f4b5c37a5f40e4d733d3bbaaef082149bee5a5ea3156a785ff64d949bd1353fa"},
- {file = "fonttools-4.60.1-cp311-cp311-win32.whl", hash = "sha256:398447f3d8c0c786cbf1209711e79080a40761eb44b27cdafffb48f52bcec258"},
- {file = "fonttools-4.60.1-cp311-cp311-win_amd64.whl", hash = "sha256:d066ea419f719ed87bc2c99a4a4bfd77c2e5949cb724588b9dd58f3fd90b92bf"},
- {file = "fonttools-4.60.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:7b0c6d57ab00dae9529f3faf187f2254ea0aa1e04215cf2f1a8ec277c96661bc"},
- {file = "fonttools-4.60.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:839565cbf14645952d933853e8ade66a463684ed6ed6c9345d0faf1f0e868877"},
- {file = "fonttools-4.60.1-cp312-cp312-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:8177ec9676ea6e1793c8a084a90b65a9f778771998eb919d05db6d4b1c0b114c"},
- {file = "fonttools-4.60.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:996a4d1834524adbb423385d5a629b868ef9d774670856c63c9a0408a3063401"},
- {file = "fonttools-4.60.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a46b2f450bc79e06ef3b6394f0c68660529ed51692606ad7f953fc2e448bc903"},
- {file = "fonttools-4.60.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:6ec722ee589e89a89f5b7574f5c45604030aa6ae24cb2c751e2707193b466fed"},
- {file = "fonttools-4.60.1-cp312-cp312-win32.whl", hash = "sha256:b2cf105cee600d2de04ca3cfa1f74f1127f8455b71dbad02b9da6ec266e116d6"},
- {file = "fonttools-4.60.1-cp312-cp312-win_amd64.whl", hash = "sha256:992775c9fbe2cf794786fa0ffca7f09f564ba3499b8fe9f2f80bd7197db60383"},
- {file = "fonttools-4.60.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:6f68576bb4bbf6060c7ab047b1574a1ebe5c50a17de62830079967b211059ebb"},
- {file = "fonttools-4.60.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:eedacb5c5d22b7097482fa834bda0dafa3d914a4e829ec83cdea2a01f8c813c4"},
- {file = "fonttools-4.60.1-cp313-cp313-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b33a7884fabd72bdf5f910d0cf46be50dce86a0362a65cfc746a4168c67eb96c"},
- {file = "fonttools-4.60.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2409d5fb7b55fd70f715e6d34e7a6e4f7511b8ad29a49d6df225ee76da76dd77"},
- {file = "fonttools-4.60.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c8651e0d4b3bdeda6602b85fdc2abbefc1b41e573ecb37b6779c4ca50753a199"},
- {file = "fonttools-4.60.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:145daa14bf24824b677b9357c5e44fd8895c2a8f53596e1b9ea3496081dc692c"},
- {file = "fonttools-4.60.1-cp313-cp313-win32.whl", hash = "sha256:2299df884c11162617a66b7c316957d74a18e3758c0274762d2cc87df7bc0272"},
- {file = "fonttools-4.60.1-cp313-cp313-win_amd64.whl", hash = "sha256:a3db56f153bd4c5c2b619ab02c5db5192e222150ce5a1bc10f16164714bc39ac"},
- {file = "fonttools-4.60.1-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:a884aef09d45ba1206712c7dbda5829562d3fea7726935d3289d343232ecb0d3"},
- {file = "fonttools-4.60.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8a44788d9d91df72d1a5eac49b31aeb887a5f4aab761b4cffc4196c74907ea85"},
- {file = "fonttools-4.60.1-cp314-cp314-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:e852d9dda9f93ad3651ae1e3bb770eac544ec93c3807888798eccddf84596537"},
- {file = "fonttools-4.60.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:154cb6ee417e417bf5f7c42fe25858c9140c26f647c7347c06f0cc2d47eff003"},
- {file = "fonttools-4.60.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:5664fd1a9ea7f244487ac8f10340c4e37664675e8667d6fee420766e0fb3cf08"},
- {file = "fonttools-4.60.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:583b7f8e3c49486e4d489ad1deacfb8d5be54a8ef34d6df824f6a171f8511d99"},
- {file = "fonttools-4.60.1-cp314-cp314-win32.whl", hash = "sha256:66929e2ea2810c6533a5184f938502cfdaea4bc3efb7130d8cc02e1c1b4108d6"},
- {file = "fonttools-4.60.1-cp314-cp314-win_amd64.whl", hash = "sha256:f3d5be054c461d6a2268831f04091dc82753176f6ea06dc6047a5e168265a987"},
- {file = "fonttools-4.60.1-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:b6379e7546ba4ae4b18f8ae2b9bc5960936007a1c0e30b342f662577e8bc3299"},
- {file = "fonttools-4.60.1-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:9d0ced62b59e0430b3690dbc5373df1c2aa7585e9a8ce38eff87f0fd993c5b01"},
- {file = "fonttools-4.60.1-cp314-cp314t-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:875cb7764708b3132637f6c5fb385b16eeba0f7ac9fa45a69d35e09b47045801"},
- {file = "fonttools-4.60.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a184b2ea57b13680ab6d5fbde99ccef152c95c06746cb7718c583abd8f945ccc"},
- {file = "fonttools-4.60.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:026290e4ec76583881763fac284aca67365e0be9f13a7fb137257096114cb3bc"},
- {file = "fonttools-4.60.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:f0e8817c7d1a0c2eedebf57ef9a9896f3ea23324769a9a2061a80fe8852705ed"},
- {file = "fonttools-4.60.1-cp314-cp314t-win32.whl", hash = "sha256:1410155d0e764a4615774e5c2c6fc516259fe3eca5882f034eb9bfdbee056259"},
- {file = "fonttools-4.60.1-cp314-cp314t-win_amd64.whl", hash = "sha256:022beaea4b73a70295b688f817ddc24ed3e3418b5036ffcd5658141184ef0d0c"},
- {file = "fonttools-4.60.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:122e1a8ada290423c493491d002f622b1992b1ab0b488c68e31c413390dc7eb2"},
- {file = "fonttools-4.60.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:a140761c4ff63d0cb9256ac752f230460ee225ccef4ad8f68affc723c88e2036"},
- {file = "fonttools-4.60.1-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0eae96373e4b7c9e45d099d7a523444e3554360927225c1cdae221a58a45b856"},
- {file = "fonttools-4.60.1-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:596ecaca36367027d525b3b426d8a8208169d09edcf8c7506aceb3a38bfb55c7"},
- {file = "fonttools-4.60.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:2ee06fc57512144d8b0445194c2da9f190f61ad51e230f14836286470c99f854"},
- {file = "fonttools-4.60.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:b42d86938e8dda1cd9a1a87a6d82f1818eaf933348429653559a458d027446da"},
- {file = "fonttools-4.60.1-cp39-cp39-win32.whl", hash = "sha256:8b4eb332f9501cb1cd3d4d099374a1e1306783ff95489a1026bde9eb02ccc34a"},
- {file = "fonttools-4.60.1-cp39-cp39-win_amd64.whl", hash = "sha256:7473a8ed9ed09aeaa191301244a5a9dbe46fe0bf54f9d6cd21d83044c3321217"},
- {file = "fonttools-4.60.1-py3-none-any.whl", hash = "sha256:906306ac7afe2156fcf0042173d6ebbb05416af70f6b370967b47f8f00103bbb"},
- {file = "fonttools-4.60.1.tar.gz", hash = "sha256:ef00af0439ebfee806b25f24c8f92109157ff3fac5731dc7867957812e87b8d9"},
+ {file = "fonttools-4.61.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7c7db70d57e5e1089a274cbb2b1fd635c9a24de809a231b154965d415d6c6d24"},
+ {file = "fonttools-4.61.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5fe9fd43882620017add5eabb781ebfbc6998ee49b35bd7f8f79af1f9f99a958"},
+ {file = "fonttools-4.61.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d8db08051fc9e7d8bc622f2112511b8107d8f27cd89e2f64ec45e9825e8288da"},
+ {file = "fonttools-4.61.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a76d4cb80f41ba94a6691264be76435e5f72f2cb3cab0b092a6212855f71c2f6"},
+ {file = "fonttools-4.61.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a13fc8aeb24bad755eea8f7f9d409438eb94e82cf86b08fe77a03fbc8f6a96b1"},
+ {file = "fonttools-4.61.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b846a1fcf8beadeb9ea4f44ec5bdde393e2f1569e17d700bfc49cd69bde75881"},
+ {file = "fonttools-4.61.1-cp310-cp310-win32.whl", hash = "sha256:78a7d3ab09dc47ac1a363a493e6112d8cabed7ba7caad5f54dbe2f08676d1b47"},
+ {file = "fonttools-4.61.1-cp310-cp310-win_amd64.whl", hash = "sha256:eff1ac3cc66c2ac7cda1e64b4e2f3ffef474b7335f92fc3833fc632d595fcee6"},
+ {file = "fonttools-4.61.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:c6604b735bb12fef8e0efd5578c9fb5d3d8532d5001ea13a19cddf295673ee09"},
+ {file = "fonttools-4.61.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5ce02f38a754f207f2f06557523cd39a06438ba3aafc0639c477ac409fc64e37"},
+ {file = "fonttools-4.61.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:77efb033d8d7ff233385f30c62c7c79271c8885d5c9657d967ede124671bbdfb"},
+ {file = "fonttools-4.61.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:75c1a6dfac6abd407634420c93864a1e274ebc1c7531346d9254c0d8f6ca00f9"},
+ {file = "fonttools-4.61.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0de30bfe7745c0d1ffa2b0b7048fb7123ad0d71107e10ee090fa0b16b9452e87"},
+ {file = "fonttools-4.61.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:58b0ee0ab5b1fc9921eccfe11d1435added19d6494dde14e323f25ad2bc30c56"},
+ {file = "fonttools-4.61.1-cp311-cp311-win32.whl", hash = "sha256:f79b168428351d11e10c5aeb61a74e1851ec221081299f4cf56036a95431c43a"},
+ {file = "fonttools-4.61.1-cp311-cp311-win_amd64.whl", hash = "sha256:fe2efccb324948a11dd09d22136fe2ac8a97d6c1347cf0b58a911dcd529f66b7"},
+ {file = "fonttools-4.61.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:f3cb4a569029b9f291f88aafc927dd53683757e640081ca8c412781ea144565e"},
+ {file = "fonttools-4.61.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:41a7170d042e8c0024703ed13b71893519a1a6d6e18e933e3ec7507a2c26a4b2"},
+ {file = "fonttools-4.61.1-cp312-cp312-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:10d88e55330e092940584774ee5e8a6971b01fc2f4d3466a1d6c158230880796"},
+ {file = "fonttools-4.61.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:15acc09befd16a0fb8a8f62bc147e1a82817542d72184acca9ce6e0aeda9fa6d"},
+ {file = "fonttools-4.61.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e6bcdf33aec38d16508ce61fd81838f24c83c90a1d1b8c68982857038673d6b8"},
+ {file = "fonttools-4.61.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5fade934607a523614726119164ff621e8c30e8fa1ffffbbd358662056ba69f0"},
+ {file = "fonttools-4.61.1-cp312-cp312-win32.whl", hash = "sha256:75da8f28eff26defba42c52986de97b22106cb8f26515b7c22443ebc9c2d3261"},
+ {file = "fonttools-4.61.1-cp312-cp312-win_amd64.whl", hash = "sha256:497c31ce314219888c0e2fce5ad9178ca83fe5230b01a5006726cdf3ac9f24d9"},
+ {file = "fonttools-4.61.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:8c56c488ab471628ff3bfa80964372fc13504ece601e0d97a78ee74126b2045c"},
+ {file = "fonttools-4.61.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:dc492779501fa723b04d0ab1f5be046797fee17d27700476edc7ee9ae535a61e"},
+ {file = "fonttools-4.61.1-cp313-cp313-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:64102ca87e84261419c3747a0d20f396eb024bdbeb04c2bfb37e2891f5fadcb5"},
+ {file = "fonttools-4.61.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4c1b526c8d3f615a7b1867f38a9410849c8f4aef078535742198e942fba0e9bd"},
+ {file = "fonttools-4.61.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:41ed4b5ec103bd306bb68f81dc166e77409e5209443e5773cb4ed837bcc9b0d3"},
+ {file = "fonttools-4.61.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b501c862d4901792adaec7c25b1ecc749e2662543f68bb194c42ba18d6eec98d"},
+ {file = "fonttools-4.61.1-cp313-cp313-win32.whl", hash = "sha256:4d7092bb38c53bbc78e9255a59158b150bcdc115a1e3b3ce0b5f267dc35dd63c"},
+ {file = "fonttools-4.61.1-cp313-cp313-win_amd64.whl", hash = "sha256:21e7c8d76f62ab13c9472ccf74515ca5b9a761d1bde3265152a6dc58700d895b"},
+ {file = "fonttools-4.61.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:fff4f534200a04b4a36e7ae3cb74493afe807b517a09e99cb4faa89a34ed6ecd"},
+ {file = "fonttools-4.61.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:d9203500f7c63545b4ce3799319fe4d9feb1a1b89b28d3cb5abd11b9dd64147e"},
+ {file = "fonttools-4.61.1-cp314-cp314-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:fa646ecec9528bef693415c79a86e733c70a4965dd938e9a226b0fc64c9d2e6c"},
+ {file = "fonttools-4.61.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:11f35ad7805edba3aac1a3710d104592df59f4b957e30108ae0ba6c10b11dd75"},
+ {file = "fonttools-4.61.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b931ae8f62db78861b0ff1ac017851764602288575d65b8e8ff1963fed419063"},
+ {file = "fonttools-4.61.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:b148b56f5de675ee16d45e769e69f87623a4944f7443850bf9a9376e628a89d2"},
+ {file = "fonttools-4.61.1-cp314-cp314-win32.whl", hash = "sha256:9b666a475a65f4e839d3d10473fad6d47e0a9db14a2f4a224029c5bfde58ad2c"},
+ {file = "fonttools-4.61.1-cp314-cp314-win_amd64.whl", hash = "sha256:4f5686e1fe5fce75d82d93c47a438a25bf0d1319d2843a926f741140b2b16e0c"},
+ {file = "fonttools-4.61.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:e76ce097e3c57c4bcb67c5aa24a0ecdbd9f74ea9219997a707a4061fbe2707aa"},
+ {file = "fonttools-4.61.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:9cfef3ab326780c04d6646f68d4b4742aae222e8b8ea1d627c74e38afcbc9d91"},
+ {file = "fonttools-4.61.1-cp314-cp314t-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:a75c301f96db737e1c5ed5fd7d77d9c34466de16095a266509e13da09751bd19"},
+ {file = "fonttools-4.61.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:91669ccac46bbc1d09e9273546181919064e8df73488ea087dcac3e2968df9ba"},
+ {file = "fonttools-4.61.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c33ab3ca9d3ccd581d58e989d67554e42d8d4ded94ab3ade3508455fe70e65f7"},
+ {file = "fonttools-4.61.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:664c5a68ec406f6b1547946683008576ef8b38275608e1cee6c061828171c118"},
+ {file = "fonttools-4.61.1-cp314-cp314t-win32.whl", hash = "sha256:aed04cabe26f30c1647ef0e8fbb207516fd40fe9472e9439695f5c6998e60ac5"},
+ {file = "fonttools-4.61.1-cp314-cp314t-win_amd64.whl", hash = "sha256:2180f14c141d2f0f3da43f3a81bc8aa4684860f6b0e6f9e165a4831f24e6a23b"},
+ {file = "fonttools-4.61.1-py3-none-any.whl", hash = "sha256:17d2bf5d541add43822bcf0c43d7d847b160c9bb01d15d5007d84e2217aaa371"},
+ {file = "fonttools-4.61.1.tar.gz", hash = "sha256:6675329885c44657f826ef01d9e4fb33b9158e9d93c537d84ad8399539bc6f69"},
]
[package.extras]
-all = ["brotli (>=1.0.1) ; platform_python_implementation == \"CPython\"", "brotlicffi (>=0.8.0) ; platform_python_implementation != \"CPython\"", "lxml (>=4.0)", "lz4 (>=1.7.4.2)", "matplotlib", "munkres ; platform_python_implementation == \"PyPy\"", "pycairo", "scipy ; platform_python_implementation != \"PyPy\"", "skia-pathops (>=0.5.0)", "sympy", "uharfbuzz (>=0.23.0)", "unicodedata2 (>=15.1.0) ; python_version <= \"3.12\"", "xattr ; sys_platform == \"darwin\"", "zopfli (>=0.1.4)"]
+all = ["brotli (>=1.0.1) ; platform_python_implementation == \"CPython\"", "brotlicffi (>=0.8.0) ; platform_python_implementation != \"CPython\"", "lxml (>=4.0)", "lz4 (>=1.7.4.2)", "matplotlib", "munkres ; platform_python_implementation == \"PyPy\"", "pycairo", "scipy ; platform_python_implementation != \"PyPy\"", "skia-pathops (>=0.5.0)", "sympy", "uharfbuzz (>=0.45.0)", "unicodedata2 (>=17.0.0) ; python_version <= \"3.14\"", "xattr ; sys_platform == \"darwin\"", "zopfli (>=0.1.4)"]
graphite = ["lz4 (>=1.7.4.2)"]
interpolatable = ["munkres ; platform_python_implementation == \"PyPy\"", "pycairo", "scipy ; platform_python_implementation != \"PyPy\""]
lxml = ["lxml (>=4.0)"]
pathops = ["skia-pathops (>=0.5.0)"]
plot = ["matplotlib"]
-repacker = ["uharfbuzz (>=0.23.0)"]
+repacker = ["uharfbuzz (>=0.45.0)"]
symfont = ["sympy"]
type1 = ["xattr ; sys_platform == \"darwin\""]
-unicode = ["unicodedata2 (>=15.1.0) ; python_version <= \"3.12\""]
+unicode = ["unicodedata2 (>=17.0.0) ; python_version <= \"3.14\""]
woff = ["brotli (>=1.0.1) ; platform_python_implementation == \"CPython\"", "brotlicffi (>=0.8.0) ; platform_python_implementation != \"CPython\"", "zopfli (>=0.1.4)"]
[[package]]
@@ -4224,14 +4267,14 @@ files = [
[[package]]
name = "marshmallow"
-version = "3.26.1"
+version = "3.26.2"
description = "A lightweight library for converting complex datatypes to and from native Python datatypes."
optional = false
python-versions = ">=3.9"
groups = ["dev"]
files = [
- {file = "marshmallow-3.26.1-py3-none-any.whl", hash = "sha256:3350409f20a70a7e4e11a27661187b77cdcaeb20abca41c1454fe33636bea09c"},
- {file = "marshmallow-3.26.1.tar.gz", hash = "sha256:e6d8affb6cb61d39d26402096dc0aee12d5a26d490a121f118d2e81dc0719dc6"},
+ {file = "marshmallow-3.26.2-py3-none-any.whl", hash = "sha256:013fa8a3c4c276c24d26d84ce934dc964e2aa794345a0f8c7e5a7191482c8a73"},
+ {file = "marshmallow-3.26.2.tar.gz", hash = "sha256:bbe2adb5a03e6e3571b573f42527c6fe926e17467833660bebd11593ab8dfd57"},
]
[package.dependencies]
@@ -5686,11 +5729,11 @@ description = "C parser in Python"
optional = false
python-versions = ">=3.8"
groups = ["main", "dev"]
+markers = "platform_python_implementation != \"PyPy\" and implementation_name != \"PyPy\""
files = [
{file = "pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc"},
{file = "pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6"},
]
-markers = {dev = "platform_python_implementation != \"PyPy\""}
[[package]]
name = "pycurl"
@@ -5954,30 +5997,45 @@ testutils = ["gitpython (>3)"]
[[package]]
name = "pynacl"
-version = "1.5.0"
+version = "1.6.2"
description = "Python binding to the Networking and Cryptography (NaCl) library"
optional = false
-python-versions = ">=3.6"
+python-versions = ">=3.8"
groups = ["main"]
files = [
- {file = "PyNaCl-1.5.0-cp36-abi3-macosx_10_10_universal2.whl", hash = "sha256:401002a4aaa07c9414132aaed7f6836ff98f59277a234704ff66878c2ee4a0d1"},
- {file = "PyNaCl-1.5.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:52cb72a79269189d4e0dc537556f4740f7f0a9ec41c1322598799b0bdad4ef92"},
- {file = "PyNaCl-1.5.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a36d4a9dda1f19ce6e03c9a784a2921a4b726b02e1c736600ca9c22029474394"},
- {file = "PyNaCl-1.5.0-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:0c84947a22519e013607c9be43706dd42513f9e6ae5d39d3613ca1e142fba44d"},
- {file = "PyNaCl-1.5.0-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:06b8f6fa7f5de8d5d2f7573fe8c863c051225a27b61e6860fd047b1775807858"},
- {file = "PyNaCl-1.5.0-cp36-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:a422368fc821589c228f4c49438a368831cb5bbc0eab5ebe1d7fac9dded6567b"},
- {file = "PyNaCl-1.5.0-cp36-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:61f642bf2378713e2c2e1de73444a3778e5f0a38be6fee0fe532fe30060282ff"},
- {file = "PyNaCl-1.5.0-cp36-abi3-win32.whl", hash = "sha256:e46dae94e34b085175f8abb3b0aaa7da40767865ac82c928eeb9e57e1ea8a543"},
- {file = "PyNaCl-1.5.0-cp36-abi3-win_amd64.whl", hash = "sha256:20f42270d27e1b6a29f54032090b972d97f0a1b0948cc52392041ef7831fee93"},
- {file = "PyNaCl-1.5.0.tar.gz", hash = "sha256:8ac7448f09ab85811607bdd21ec2464495ac8b7c66d146bf545b0f08fb9220ba"},
+ {file = "pynacl-1.6.2-cp314-cp314t-macosx_10_10_universal2.whl", hash = "sha256:622d7b07cc5c02c666795792931b50c91f3ce3c2649762efb1ef0d5684c81594"},
+ {file = "pynacl-1.6.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d071c6a9a4c94d79eb665db4ce5cedc537faf74f2355e4d502591d850d3913c0"},
+ {file = "pynacl-1.6.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fe9847ca47d287af41e82be1dd5e23023d3c31a951da134121ab02e42ac218c9"},
+ {file = "pynacl-1.6.2-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:04316d1fc625d860b6c162fff704eb8426b1a8bcd3abacea11142cbd99a6b574"},
+ {file = "pynacl-1.6.2-cp314-cp314t-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:44081faff368d6c5553ccf55322ef2819abb40e25afaec7e740f159f74813634"},
+ {file = "pynacl-1.6.2-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:a9f9932d8d2811ce1a8ffa79dcbdf3970e7355b5c8eb0c1a881a57e7f7d96e88"},
+ {file = "pynacl-1.6.2-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:bc4a36b28dd72fb4845e5d8f9760610588a96d5a51f01d84d8c6ff9849968c14"},
+ {file = "pynacl-1.6.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:3bffb6d0f6becacb6526f8f42adfb5efb26337056ee0831fb9a7044d1a964444"},
+ {file = "pynacl-1.6.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:2fef529ef3ee487ad8113d287a593fa26f48ee3620d92ecc6f1d09ea38e0709b"},
+ {file = "pynacl-1.6.2-cp314-cp314t-win32.whl", hash = "sha256:a84bf1c20339d06dc0c85d9aea9637a24f718f375d861b2668b2f9f96fa51145"},
+ {file = "pynacl-1.6.2-cp314-cp314t-win_amd64.whl", hash = "sha256:320ef68a41c87547c91a8b58903c9caa641ab01e8512ce291085b5fe2fcb7590"},
+ {file = "pynacl-1.6.2-cp314-cp314t-win_arm64.whl", hash = "sha256:d29bfe37e20e015a7d8b23cfc8bd6aa7909c92a1b8f41ee416bbb3e79ef182b2"},
+ {file = "pynacl-1.6.2-cp38-abi3-macosx_10_10_universal2.whl", hash = "sha256:c949ea47e4206af7c8f604b8278093b674f7c79ed0d4719cc836902bf4517465"},
+ {file = "pynacl-1.6.2-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8845c0631c0be43abdd865511c41eab235e0be69c81dc66a50911594198679b0"},
+ {file = "pynacl-1.6.2-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:22de65bb9010a725b0dac248f353bb072969c94fa8d6b1f34b87d7953cf7bbe4"},
+ {file = "pynacl-1.6.2-cp38-abi3-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:46065496ab748469cdd999246d17e301b2c24ae2fdf739132e580a0e94c94a87"},
+ {file = "pynacl-1.6.2-cp38-abi3-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8a66d6fb6ae7661c58995f9c6435bda2b1e68b54b598a6a10247bfcdadac996c"},
+ {file = "pynacl-1.6.2-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:26bfcd00dcf2cf160f122186af731ae30ab120c18e8375684ec2670dccd28130"},
+ {file = "pynacl-1.6.2-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:c8a231e36ec2cab018c4ad4358c386e36eede0319a0c41fed24f840b1dac59f6"},
+ {file = "pynacl-1.6.2-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:68be3a09455743ff9505491220b64440ced8973fe930f270c8e07ccfa25b1f9e"},
+ {file = "pynacl-1.6.2-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:8b097553b380236d51ed11356c953bf8ce36a29a3e596e934ecabe76c985a577"},
+ {file = "pynacl-1.6.2-cp38-abi3-win32.whl", hash = "sha256:5811c72b473b2f38f7e2a3dc4f8642e3a3e9b5e7317266e4ced1fba85cae41aa"},
+ {file = "pynacl-1.6.2-cp38-abi3-win_amd64.whl", hash = "sha256:62985f233210dee6548c223301b6c25440852e13d59a8b81490203c3227c5ba0"},
+ {file = "pynacl-1.6.2-cp38-abi3-win_arm64.whl", hash = "sha256:834a43af110f743a754448463e8fd61259cd4ab5bbedcf70f9dabad1d28a394c"},
+ {file = "pynacl-1.6.2.tar.gz", hash = "sha256:018494d6d696ae03c7e656e5e74cdfd8ea1326962cc401bcf018f1ed8436811c"},
]
[package.dependencies]
-cffi = ">=1.4.1"
+cffi = {version = ">=2.0.0", markers = "platform_python_implementation != \"PyPy\" and python_version >= \"3.9\""}
[package.extras]
-docs = ["sphinx (>=1.6.5)", "sphinx-rtd-theme"]
-tests = ["hypothesis (>=3.27.0)", "pytest (>=3.2.1,!=3.3.0)"]
+docs = ["sphinx (<7)", "sphinx_rtd_theme"]
+tests = ["hypothesis (>=3.27.0)", "pytest (>=7.4.0)", "pytest-cov (>=2.10.1)", "pytest-xdist (>=3.5.0)"]
[[package]]
name = "pyopenssl"
@@ -7015,18 +7073,18 @@ files = [
[[package]]
name = "sqlparse"
-version = "0.5.3"
+version = "0.5.5"
description = "A non-validating SQL parser."
optional = false
python-versions = ">=3.8"
groups = ["main", "dev"]
files = [
- {file = "sqlparse-0.5.3-py3-none-any.whl", hash = "sha256:cf2196ed3418f3ba5de6af7e82c694a9fbdbfecccdfc72e281548517081f16ca"},
- {file = "sqlparse-0.5.3.tar.gz", hash = "sha256:09f67787f56a0b16ecdbde1bfc7f5d9c3371ca683cfeaa8e6ff60b4807ec9272"},
+ {file = "sqlparse-0.5.5-py3-none-any.whl", hash = "sha256:12a08b3bf3eec877c519589833aed092e2444e68240a3577e8e26148acc7b1ba"},
+ {file = "sqlparse-0.5.5.tar.gz", hash = "sha256:e20d4a9b0b8585fdf63b10d30066c7c94c5d7a7ec47c889a2d83a3caa93ff28e"},
]
[package.extras]
-dev = ["build", "hatch"]
+dev = ["build"]
doc = ["sphinx"]
[[package]]
@@ -7233,21 +7291,21 @@ files = [
[[package]]
name = "urllib3"
-version = "2.5.0"
+version = "2.6.3"
description = "HTTP library with thread-safe connection pooling, file post, and more."
optional = false
python-versions = ">=3.9"
groups = ["main", "dev"]
files = [
- {file = "urllib3-2.5.0-py3-none-any.whl", hash = "sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc"},
- {file = "urllib3-2.5.0.tar.gz", hash = "sha256:3fc47733c7e419d4bc3f6b3dc2b4f890bb743906a30d56ba4a5bfa4bbff92760"},
+ {file = "urllib3-2.6.3-py3-none-any.whl", hash = "sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4"},
+ {file = "urllib3-2.6.3.tar.gz", hash = "sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed"},
]
[package.extras]
-brotli = ["brotli (>=1.0.9) ; platform_python_implementation == \"CPython\"", "brotlicffi (>=0.8.0) ; platform_python_implementation != \"CPython\""]
+brotli = ["brotli (>=1.2.0) ; platform_python_implementation == \"CPython\"", "brotlicffi (>=1.2.0.0) ; platform_python_implementation != \"CPython\""]
h2 = ["h2 (>=4,<5)"]
socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"]
-zstd = ["zstandard (>=0.18.0)"]
+zstd = ["backports-zstd (>=1.0.0) ; python_version < \"3.14\""]
[[package]]
name = "uuid6"
@@ -7316,18 +7374,18 @@ test = ["websockets"]
[[package]]
name = "werkzeug"
-version = "3.1.3"
+version = "3.1.5"
description = "The comprehensive WSGI web application library."
optional = false
python-versions = ">=3.9"
groups = ["main"]
files = [
- {file = "werkzeug-3.1.3-py3-none-any.whl", hash = "sha256:54b78bf3716d19a65be4fceccc0d1d7b89e608834989dfae50ea87564639213e"},
- {file = "werkzeug-3.1.3.tar.gz", hash = "sha256:60723ce945c19328679790e3282cc758aa4a6040e4bb330f53d30fa546d44746"},
+ {file = "werkzeug-3.1.5-py3-none-any.whl", hash = "sha256:5111e36e91086ece91f93268bb39b4a35c1e6f1feac762c9c822ded0a4e322dc"},
+ {file = "werkzeug-3.1.5.tar.gz", hash = "sha256:6a548b0e88955dd07ccb25539d7d0cc97417ee9e179677d22c7041c8f078ce67"},
]
[package.dependencies]
-MarkupSafe = ">=2.1.1"
+markupsafe = ">=2.1.1"
[package.extras]
watchdog = ["watchdog (>=2.3)"]
@@ -7846,4 +7904,4 @@ files = [
[metadata]
lock-version = "2.1"
python-versions = ">=3.11,<3.13"
-content-hash = "77ef098291cb8631565a1ab5027ce33e7fcb5a04883dc7160bf373eac9e1fb49"
+content-hash = "c40ff4d1cb06db047a7a39c35f6c765c259583c203da549146a0ed2e6d17a727"
diff --git a/api/pyproject.toml b/api/pyproject.toml
index 038fbc69a1..889a0eb519 100644
--- a/api/pyproject.toml
+++ b/api/pyproject.toml
@@ -7,7 +7,7 @@ authors = [{name = "Prowler Engineering", email = "engineering@prowler.com"}]
dependencies = [
"celery[pytest] (>=5.4.0,<6.0.0)",
"dj-rest-auth[with_social,jwt] (==7.0.1)",
- "django (==5.1.14)",
+ "django (==5.1.15)",
"django-allauth[saml] (>=65.8.0,<66.0.0)",
"django-celery-beat (>=2.7.0,<3.0.0)",
"django-celery-results (>=2.5.1,<3.0.0)",
@@ -36,7 +36,10 @@ dependencies = [
"drf-simple-apikey (==2.2.1)",
"matplotlib (>=3.10.6,<4.0.0)",
"reportlab (>=4.4.4,<5.0.0)",
- "gevent (>=25.9.1,<26.0.0)"
+ "gevent (>=25.9.1,<26.0.0)",
+ "werkzeug (>=3.1.4)",
+ "sqlparse (>=0.5.4)",
+ "fonttools (>=4.60.2)"
]
description = "Prowler's API (Django/DRF)"
license = "Apache-2.0"
diff --git a/api/src/backend/api/filters.py b/api/src/backend/api/filters.py
index f626732477..fdb2282e12 100644
--- a/api/src/backend/api/filters.py
+++ b/api/src/backend/api/filters.py
@@ -37,6 +37,7 @@ from api.models import (
PermissionChoices,
Processor,
Provider,
+ ProviderComplianceScore,
ProviderGroup,
ProviderSecret,
Resource,
@@ -92,10 +93,62 @@ class ChoiceInFilter(BaseInFilter, ChoiceFilter):
pass
+class BaseProviderFilter(FilterSet):
+ """
+ Abstract base filter for models with direct FK to Provider.
+
+ Provides standard provider_id and provider_type filters.
+ Subclasses must define Meta.model.
+ """
+
+ provider_id = UUIDFilter(field_name="provider__id", lookup_expr="exact")
+ provider_id__in = UUIDInFilter(field_name="provider__id", lookup_expr="in")
+ provider_type = ChoiceFilter(
+ field_name="provider__provider", choices=Provider.ProviderChoices.choices
+ )
+ provider_type__in = ChoiceInFilter(
+ field_name="provider__provider",
+ choices=Provider.ProviderChoices.choices,
+ lookup_expr="in",
+ )
+
+ class Meta:
+ abstract = True
+ fields = {}
+
+
+class BaseScanProviderFilter(FilterSet):
+ """
+ Abstract base filter for models with FK to Scan (and Scan has FK to Provider).
+
+ Provides standard provider_id and provider_type filters via scan relationship.
+ Subclasses must define Meta.model.
+ """
+
+ provider_id = UUIDFilter(field_name="scan__provider__id", lookup_expr="exact")
+ provider_id__in = UUIDInFilter(field_name="scan__provider__id", lookup_expr="in")
+ provider_type = ChoiceFilter(
+ field_name="scan__provider__provider", choices=Provider.ProviderChoices.choices
+ )
+ provider_type__in = ChoiceInFilter(
+ field_name="scan__provider__provider",
+ choices=Provider.ProviderChoices.choices,
+ lookup_expr="in",
+ )
+
+ class Meta:
+ abstract = True
+ fields = {}
+
+
class CommonFindingFilters(FilterSet):
# We filter providers from the scan in findings
+ # Both 'provider' and 'provider_id' parameters are supported for API consistency
+ # Frontend uses 'provider_id' uniformly across all endpoints
provider = UUIDFilter(field_name="scan__provider__id", lookup_expr="exact")
provider__in = UUIDInFilter(field_name="scan__provider__id", lookup_expr="in")
+ provider_id = UUIDFilter(field_name="scan__provider__id", lookup_expr="exact")
+ provider_id__in = UUIDInFilter(field_name="scan__provider__id", lookup_expr="in")
provider_type = ChoiceFilter(
choices=Provider.ProviderChoices.choices, field_name="scan__provider__provider"
)
@@ -1086,39 +1139,25 @@ class ThreatScoreSnapshotFilter(FilterSet):
}
-class AttackSurfaceOverviewFilter(FilterSet):
+class AttackSurfaceOverviewFilter(BaseScanProviderFilter):
"""Filter for attack surface overview aggregations by provider."""
- provider_id = UUIDFilter(field_name="scan__provider__id", lookup_expr="exact")
- provider_id__in = UUIDInFilter(field_name="scan__provider__id", lookup_expr="in")
- provider_type = ChoiceFilter(
- field_name="scan__provider__provider", choices=Provider.ProviderChoices.choices
- )
- provider_type__in = ChoiceInFilter(
- field_name="scan__provider__provider",
- choices=Provider.ProviderChoices.choices,
- lookup_expr="in",
- )
-
- class Meta:
+ class Meta(BaseScanProviderFilter.Meta):
model = AttackSurfaceOverview
- fields = {}
-class CategoryOverviewFilter(FilterSet):
- provider_id = UUIDFilter(field_name="scan__provider__id", lookup_expr="exact")
- provider_id__in = UUIDInFilter(field_name="scan__provider__id", lookup_expr="in")
- provider_type = ChoiceFilter(
- field_name="scan__provider__provider", choices=Provider.ProviderChoices.choices
- )
- provider_type__in = ChoiceInFilter(
- field_name="scan__provider__provider",
- choices=Provider.ProviderChoices.choices,
- lookup_expr="in",
- )
+class CategoryOverviewFilter(BaseScanProviderFilter):
+ """Filter for category overview aggregations by provider."""
+
category = CharFilter(field_name="category", lookup_expr="exact")
category__in = CharInFilter(field_name="category", lookup_expr="in")
- class Meta:
+ class Meta(BaseScanProviderFilter.Meta):
model = ScanCategorySummary
- fields = {}
+
+
+class ComplianceWatchlistFilter(BaseProviderFilter):
+ """Filter for compliance watchlist overview by provider."""
+
+ class Meta(BaseProviderFilter.Meta):
+ model = ProviderComplianceScore
diff --git a/api/src/backend/api/migrations/0066_provider_compliance_score.py b/api/src/backend/api/migrations/0066_provider_compliance_score.py
new file mode 100644
index 0000000000..f9a6483e4f
--- /dev/null
+++ b/api/src/backend/api/migrations/0066_provider_compliance_score.py
@@ -0,0 +1,94 @@
+import uuid
+
+import django.db.models.deletion
+from django.db import migrations, models
+
+import api.db_utils
+import api.rls
+
+
+class Migration(migrations.Migration):
+ dependencies = [
+ ("api", "0065_alibabacloud_provider"),
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name="ProviderComplianceScore",
+ fields=[
+ (
+ "id",
+ models.UUIDField(
+ default=uuid.uuid4,
+ editable=False,
+ primary_key=True,
+ serialize=False,
+ ),
+ ),
+ ("compliance_id", models.TextField()),
+ ("requirement_id", models.TextField()),
+ (
+ "requirement_status",
+ api.db_utils.StatusEnumField(
+ choices=[
+ ("FAIL", "Fail"),
+ ("PASS", "Pass"),
+ ("MANUAL", "Manual"),
+ ]
+ ),
+ ),
+ ("scan_completed_at", models.DateTimeField()),
+ (
+ "provider",
+ models.ForeignKey(
+ on_delete=django.db.models.deletion.CASCADE,
+ related_name="compliance_scores",
+ related_query_name="compliance_score",
+ to="api.provider",
+ ),
+ ),
+ (
+ "scan",
+ models.ForeignKey(
+ on_delete=django.db.models.deletion.CASCADE,
+ related_name="compliance_scores",
+ related_query_name="compliance_score",
+ to="api.scan",
+ ),
+ ),
+ (
+ "tenant",
+ models.ForeignKey(
+ on_delete=django.db.models.deletion.CASCADE,
+ to="api.tenant",
+ ),
+ ),
+ ],
+ options={
+ "db_table": "provider_compliance_scores",
+ "abstract": False,
+ },
+ ),
+ migrations.AddConstraint(
+ model_name="providercompliancescore",
+ constraint=models.UniqueConstraint(
+ fields=("tenant_id", "provider_id", "compliance_id", "requirement_id"),
+ name="unique_provider_compliance_req",
+ ),
+ ),
+ migrations.AddConstraint(
+ model_name="providercompliancescore",
+ constraint=api.rls.RowLevelSecurityConstraint(
+ "tenant_id",
+ name="rls_on_providercompliancescore",
+ statements=["SELECT", "INSERT", "UPDATE", "DELETE"],
+ ),
+ ),
+ migrations.AddIndex(
+ model_name="providercompliancescore",
+ index=models.Index(
+ fields=["tenant_id", "provider_id", "compliance_id"],
+ name="pcs_tenant_prov_comp_idx",
+ ),
+ ),
+ ]
diff --git a/api/src/backend/api/migrations/0067_tenant_compliance_summary.py b/api/src/backend/api/migrations/0067_tenant_compliance_summary.py
new file mode 100644
index 0000000000..bd753ca575
--- /dev/null
+++ b/api/src/backend/api/migrations/0067_tenant_compliance_summary.py
@@ -0,0 +1,61 @@
+import uuid
+
+import django.db.models.deletion
+from django.db import migrations, models
+
+import api.rls
+
+
+class Migration(migrations.Migration):
+ dependencies = [
+ ("api", "0066_provider_compliance_score"),
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name="TenantComplianceSummary",
+ fields=[
+ (
+ "id",
+ models.UUIDField(
+ default=uuid.uuid4,
+ editable=False,
+ primary_key=True,
+ serialize=False,
+ ),
+ ),
+ ("compliance_id", models.TextField()),
+ ("requirements_passed", models.IntegerField(default=0)),
+ ("requirements_failed", models.IntegerField(default=0)),
+ ("requirements_manual", models.IntegerField(default=0)),
+ ("total_requirements", models.IntegerField(default=0)),
+ ("updated_at", models.DateTimeField(auto_now=True)),
+ (
+ "tenant",
+ models.ForeignKey(
+ on_delete=django.db.models.deletion.CASCADE,
+ to="api.tenant",
+ ),
+ ),
+ ],
+ options={
+ "db_table": "tenant_compliance_summaries",
+ "abstract": False,
+ },
+ ),
+ migrations.AddConstraint(
+ model_name="tenantcompliancesummary",
+ constraint=models.UniqueConstraint(
+ fields=("tenant_id", "compliance_id"),
+ name="unique_tenant_compliance_summary",
+ ),
+ ),
+ migrations.AddConstraint(
+ model_name="tenantcompliancesummary",
+ constraint=api.rls.RowLevelSecurityConstraint(
+ "tenant_id",
+ name="rls_on_tenantcompliancesummary",
+ statements=["SELECT", "INSERT", "UPDATE", "DELETE"],
+ ),
+ ),
+ ]
diff --git a/api/src/backend/api/models.py b/api/src/backend/api/models.py
index 464207e111..aa568bd17c 100644
--- a/api/src/backend/api/models.py
+++ b/api/src/backend/api/models.py
@@ -2605,3 +2605,92 @@ class AttackSurfaceOverview(RowLevelSecurityProtectedModel):
class JSONAPIMeta:
resource_name = "attack-surface-overviews"
+
+
+class ProviderComplianceScore(RowLevelSecurityProtectedModel):
+ """
+ Compliance requirement status from latest completed scan per provider.
+
+ Used for efficient compliance watchlist queries with FAIL-dominant aggregation
+ across multiple providers. Updated via atomic upsert after each scan completion.
+ """
+
+ id = models.UUIDField(primary_key=True, default=uuid4, editable=False)
+
+ scan = models.ForeignKey(
+ Scan,
+ on_delete=models.CASCADE,
+ related_name="compliance_scores",
+ related_query_name="compliance_score",
+ )
+
+ provider = models.ForeignKey(
+ Provider,
+ on_delete=models.CASCADE,
+ related_name="compliance_scores",
+ related_query_name="compliance_score",
+ )
+
+ compliance_id = models.TextField()
+ requirement_id = models.TextField()
+ requirement_status = StatusEnumField(choices=StatusChoices)
+
+ scan_completed_at = models.DateTimeField()
+
+ class Meta(RowLevelSecurityProtectedModel.Meta):
+ db_table = "provider_compliance_scores"
+
+ constraints = [
+ models.UniqueConstraint(
+ fields=("tenant_id", "provider_id", "compliance_id", "requirement_id"),
+ name="unique_provider_compliance_req",
+ ),
+ RowLevelSecurityConstraint(
+ field="tenant_id",
+ name="rls_on_%(class)s",
+ statements=["SELECT", "INSERT", "UPDATE", "DELETE"],
+ ),
+ ]
+
+ indexes = [
+ models.Index(
+ fields=["tenant_id", "provider_id", "compliance_id"],
+ name="pcs_tenant_prov_comp_idx",
+ ),
+ ]
+
+
+class TenantComplianceSummary(RowLevelSecurityProtectedModel):
+ """
+ Pre-aggregated compliance counts per tenant with FAIL-dominant logic applied.
+
+ One row per (tenant, compliance_id). Used for fast watchlist queries when
+ no provider filter is applied. Recalculated after each scan by aggregating
+ across all providers with FAIL-dominant logic at requirement level.
+ """
+
+ id = models.UUIDField(primary_key=True, default=uuid4, editable=False)
+
+ compliance_id = models.TextField()
+
+ requirements_passed = models.IntegerField(default=0)
+ requirements_failed = models.IntegerField(default=0)
+ requirements_manual = models.IntegerField(default=0)
+ total_requirements = models.IntegerField(default=0)
+
+ updated_at = models.DateTimeField(auto_now=True)
+
+ class Meta(RowLevelSecurityProtectedModel.Meta):
+ db_table = "tenant_compliance_summaries"
+
+ constraints = [
+ models.UniqueConstraint(
+ fields=("tenant_id", "compliance_id"),
+ name="unique_tenant_compliance_summary",
+ ),
+ RowLevelSecurityConstraint(
+ field="tenant_id",
+ name="rls_on_%(class)s",
+ statements=["SELECT", "INSERT", "UPDATE", "DELETE"],
+ ),
+ ]
diff --git a/api/src/backend/api/specs/v1.yaml b/api/src/backend/api/specs/v1.yaml
index 2c2bd8f027..2e238a5ad4 100644
--- a/api/src/backend/api/specs/v1.yaml
+++ b/api/src/backend/api/specs/v1.yaml
@@ -879,12 +879,28 @@ paths:
description: Multiple values may be separated by commas.
explode: false
style: form
+ - in: query
+ name: filter[provider_id]
+ schema:
+ type: string
+ format: uuid
+ - in: query
+ name: filter[provider_id__in]
+ schema:
+ type: array
+ items:
+ type: string
+ format: uuid
+ description: Multiple values may be separated by commas.
+ explode: false
+ style: form
- in: query
name: filter[provider_type]
schema:
type: string
- x-spec-enum-id: eca8c51e6bd28935
+ x-spec-enum-id: 684bf4173d2b754f
enum:
+ - alibabacloud
- aws
- azure
- gcp
@@ -894,7 +910,6 @@ paths:
- m365
- mongodbatlas
- oraclecloud
- - alibabacloud
description: |-
* `aws` - AWS
* `azure` - Azure
@@ -912,8 +927,9 @@ paths:
type: array
items:
type: string
- x-spec-enum-id: eca8c51e6bd28935
+ x-spec-enum-id: 684bf4173d2b754f
enum:
+ - alibabacloud
- aws
- azure
- gcp
@@ -923,7 +939,6 @@ paths:
- m365
- mongodbatlas
- oraclecloud
- - alibabacloud
description: |-
Multiple values may be separated by commas.
@@ -1436,12 +1451,28 @@ paths:
description: Multiple values may be separated by commas.
explode: false
style: form
+ - in: query
+ name: filter[provider_id]
+ schema:
+ type: string
+ format: uuid
+ - in: query
+ name: filter[provider_id__in]
+ schema:
+ type: array
+ items:
+ type: string
+ format: uuid
+ description: Multiple values may be separated by commas.
+ explode: false
+ style: form
- in: query
name: filter[provider_type]
schema:
type: string
- x-spec-enum-id: eca8c51e6bd28935
+ x-spec-enum-id: 684bf4173d2b754f
enum:
+ - alibabacloud
- aws
- azure
- gcp
@@ -1451,7 +1482,6 @@ paths:
- m365
- mongodbatlas
- oraclecloud
- - alibabacloud
description: |-
* `aws` - AWS
* `azure` - Azure
@@ -1469,8 +1499,9 @@ paths:
type: array
items:
type: string
- x-spec-enum-id: eca8c51e6bd28935
+ x-spec-enum-id: 684bf4173d2b754f
enum:
+ - alibabacloud
- aws
- azure
- gcp
@@ -1480,7 +1511,6 @@ paths:
- m365
- mongodbatlas
- oraclecloud
- - alibabacloud
description: |-
Multiple values may be separated by commas.
@@ -1901,12 +1931,28 @@ paths:
description: Multiple values may be separated by commas.
explode: false
style: form
+ - in: query
+ name: filter[provider_id]
+ schema:
+ type: string
+ format: uuid
+ - in: query
+ name: filter[provider_id__in]
+ schema:
+ type: array
+ items:
+ type: string
+ format: uuid
+ description: Multiple values may be separated by commas.
+ explode: false
+ style: form
- in: query
name: filter[provider_type]
schema:
type: string
- x-spec-enum-id: eca8c51e6bd28935
+ x-spec-enum-id: 684bf4173d2b754f
enum:
+ - alibabacloud
- aws
- azure
- gcp
@@ -1916,7 +1962,6 @@ paths:
- m365
- mongodbatlas
- oraclecloud
- - alibabacloud
description: |-
* `aws` - AWS
* `azure` - Azure
@@ -1934,8 +1979,9 @@ paths:
type: array
items:
type: string
- x-spec-enum-id: eca8c51e6bd28935
+ x-spec-enum-id: 684bf4173d2b754f
enum:
+ - alibabacloud
- aws
- azure
- gcp
@@ -1945,7 +1991,6 @@ paths:
- m365
- mongodbatlas
- oraclecloud
- - alibabacloud
description: |-
Multiple values may be separated by commas.
@@ -2364,12 +2409,28 @@ paths:
description: Multiple values may be separated by commas.
explode: false
style: form
+ - in: query
+ name: filter[provider_id]
+ schema:
+ type: string
+ format: uuid
+ - in: query
+ name: filter[provider_id__in]
+ schema:
+ type: array
+ items:
+ type: string
+ format: uuid
+ description: Multiple values may be separated by commas.
+ explode: false
+ style: form
- in: query
name: filter[provider_type]
schema:
type: string
- x-spec-enum-id: eca8c51e6bd28935
+ x-spec-enum-id: 684bf4173d2b754f
enum:
+ - alibabacloud
- aws
- azure
- gcp
@@ -2379,7 +2440,6 @@ paths:
- m365
- mongodbatlas
- oraclecloud
- - alibabacloud
description: |-
* `aws` - AWS
* `azure` - Azure
@@ -2397,8 +2457,9 @@ paths:
type: array
items:
type: string
- x-spec-enum-id: eca8c51e6bd28935
+ x-spec-enum-id: 684bf4173d2b754f
enum:
+ - alibabacloud
- aws
- azure
- gcp
@@ -2408,7 +2469,6 @@ paths:
- m365
- mongodbatlas
- oraclecloud
- - alibabacloud
description: |-
Multiple values may be separated by commas.
@@ -2815,12 +2875,28 @@ paths:
description: Multiple values may be separated by commas.
explode: false
style: form
+ - in: query
+ name: filter[provider_id]
+ schema:
+ type: string
+ format: uuid
+ - in: query
+ name: filter[provider_id__in]
+ schema:
+ type: array
+ items:
+ type: string
+ format: uuid
+ description: Multiple values may be separated by commas.
+ explode: false
+ style: form
- in: query
name: filter[provider_type]
schema:
type: string
- x-spec-enum-id: eca8c51e6bd28935
+ x-spec-enum-id: 684bf4173d2b754f
enum:
+ - alibabacloud
- aws
- azure
- gcp
@@ -2830,7 +2906,6 @@ paths:
- m365
- mongodbatlas
- oraclecloud
- - alibabacloud
description: |-
* `aws` - AWS
* `azure` - Azure
@@ -2848,8 +2923,9 @@ paths:
type: array
items:
type: string
- x-spec-enum-id: eca8c51e6bd28935
+ x-spec-enum-id: 684bf4173d2b754f
enum:
+ - alibabacloud
- aws
- azure
- gcp
@@ -2859,7 +2935,6 @@ paths:
- m365
- mongodbatlas
- oraclecloud
- - alibabacloud
description: |-
Multiple values may be separated by commas.
@@ -4627,8 +4702,9 @@ paths:
name: filter[provider_type]
schema:
type: string
- x-spec-enum-id: eca8c51e6bd28935
+ x-spec-enum-id: 684bf4173d2b754f
enum:
+ - alibabacloud
- aws
- azure
- gcp
@@ -4648,14 +4724,16 @@ paths:
* `mongodbatlas` - MongoDB Atlas
* `iac` - IaC
* `oraclecloud` - Oracle Cloud Infrastructure
+ * `alibabacloud` - Alibaba Cloud
- in: query
name: filter[provider_type__in]
schema:
type: array
items:
type: string
- x-spec-enum-id: eca8c51e6bd28935
+ x-spec-enum-id: 684bf4173d2b754f
enum:
+ - alibabacloud
- aws
- azure
- gcp
@@ -4677,6 +4755,7 @@ paths:
* `mongodbatlas` - MongoDB Atlas
* `iac` - IaC
* `oraclecloud` - Oracle Cloud Infrastructure
+ * `alibabacloud` - Alibaba Cloud
explode: false
style: form
- name: filter[search]
@@ -4782,8 +4861,9 @@ paths:
name: filter[provider_type]
schema:
type: string
- x-spec-enum-id: eca8c51e6bd28935
+ x-spec-enum-id: 684bf4173d2b754f
enum:
+ - alibabacloud
- aws
- azure
- gcp
@@ -4803,14 +4883,16 @@ paths:
* `mongodbatlas` - MongoDB Atlas
* `iac` - IaC
* `oraclecloud` - Oracle Cloud Infrastructure
+ * `alibabacloud` - Alibaba Cloud
- in: query
name: filter[provider_type__in]
schema:
type: array
items:
type: string
- x-spec-enum-id: eca8c51e6bd28935
+ x-spec-enum-id: 684bf4173d2b754f
enum:
+ - alibabacloud
- aws
- azure
- gcp
@@ -4832,6 +4914,7 @@ paths:
* `mongodbatlas` - MongoDB Atlas
* `iac` - IaC
* `oraclecloud` - Oracle Cloud Infrastructure
+ * `alibabacloud` - Alibaba Cloud
explode: false
style: form
- name: filter[search]
@@ -4883,6 +4966,42 @@ paths:
schema:
$ref: '#/components/schemas/PaginatedCategoryOverviewList'
description: ''
+ /api/v1/overviews/compliance-watchlist:
+ get:
+ operationId: overviews_compliance_watchlist_retrieve
+ description: |-
+ Get compliance watchlist overview with FAIL-dominant aggregation.
+
+ Without filters: uses pre-aggregated TenantComplianceSummary (~70 rows).
+ With provider filters: queries ProviderComplianceScore with FAIL-dominant logic.
+ parameters:
+ - in: query
+ name: fields[compliance-watchlist-overviews]
+ schema:
+ type: array
+ items:
+ type: string
+ enum:
+ - id
+ - compliance_id
+ - requirements_passed
+ - requirements_failed
+ - requirements_manual
+ - total_requirements
+ description: endpoint return only specific fields in the response on a per-type
+ basis by including a fields[TYPE] query parameter.
+ explode: false
+ tags:
+ - Overview
+ security:
+ - JWT or API Key: []
+ responses:
+ '200':
+ content:
+ application/vnd.api+json:
+ schema:
+ $ref: '#/components/schemas/ComplianceWatchlistOverviewResponse'
+ description: ''
/api/v1/overviews/findings:
get:
operationId: overviews_findings_retrieve
@@ -4956,8 +5075,9 @@ paths:
name: filter[provider_type]
schema:
type: string
- x-spec-enum-id: eca8c51e6bd28935
+ x-spec-enum-id: 684bf4173d2b754f
enum:
+ - alibabacloud
- aws
- azure
- gcp
@@ -4967,7 +5087,6 @@ paths:
- m365
- mongodbatlas
- oraclecloud
- - alibabacloud
description: |-
* `aws` - AWS
* `azure` - Azure
@@ -4985,8 +5104,9 @@ paths:
type: array
items:
type: string
- x-spec-enum-id: eca8c51e6bd28935
+ x-spec-enum-id: 684bf4173d2b754f
enum:
+ - alibabacloud
- aws
- azure
- gcp
@@ -4996,7 +5116,6 @@ paths:
- m365
- mongodbatlas
- oraclecloud
- - alibabacloud
description: |-
Multiple values may be separated by commas.
@@ -5149,8 +5268,9 @@ paths:
name: filter[provider_type]
schema:
type: string
- x-spec-enum-id: eca8c51e6bd28935
+ x-spec-enum-id: 684bf4173d2b754f
enum:
+ - alibabacloud
- aws
- azure
- gcp
@@ -5160,7 +5280,6 @@ paths:
- m365
- mongodbatlas
- oraclecloud
- - alibabacloud
description: |-
* `aws` - AWS
* `azure` - Azure
@@ -5178,8 +5297,9 @@ paths:
type: array
items:
type: string
- x-spec-enum-id: eca8c51e6bd28935
+ x-spec-enum-id: 684bf4173d2b754f
enum:
+ - alibabacloud
- aws
- azure
- gcp
@@ -5189,7 +5309,6 @@ paths:
- m365
- mongodbatlas
- oraclecloud
- - alibabacloud
description: |-
Multiple values may be separated by commas.
@@ -5338,6 +5457,7 @@ paths:
schema:
type: string
enum:
+ - alibabacloud
- aws
- azure
- gcp
@@ -5347,7 +5467,6 @@ paths:
- m365
- mongodbatlas
- oraclecloud
- - alibabacloud
description: |-
* `aws` - AWS
* `azure` - Azure
@@ -5366,6 +5485,7 @@ paths:
items:
type: string
enum:
+ - alibabacloud
- aws
- azure
- gcp
@@ -5375,7 +5495,6 @@ paths:
- m365
- mongodbatlas
- oraclecloud
- - alibabacloud
description: |-
Multiple values may be separated by commas.
@@ -5564,8 +5683,9 @@ paths:
name: filter[provider_type]
schema:
type: string
- x-spec-enum-id: eca8c51e6bd28935
+ x-spec-enum-id: 684bf4173d2b754f
enum:
+ - alibabacloud
- aws
- azure
- gcp
@@ -5575,7 +5695,6 @@ paths:
- m365
- mongodbatlas
- oraclecloud
- - alibabacloud
description: |-
* `aws` - AWS
* `azure` - Azure
@@ -5593,8 +5712,9 @@ paths:
type: array
items:
type: string
- x-spec-enum-id: eca8c51e6bd28935
+ x-spec-enum-id: 684bf4173d2b754f
enum:
+ - alibabacloud
- aws
- azure
- gcp
@@ -5604,7 +5724,6 @@ paths:
- m365
- mongodbatlas
- oraclecloud
- - alibabacloud
description: |-
Multiple values may be separated by commas.
@@ -5740,8 +5859,9 @@ paths:
name: filter[provider_type]
schema:
type: string
- x-spec-enum-id: eca8c51e6bd28935
+ x-spec-enum-id: 684bf4173d2b754f
enum:
+ - alibabacloud
- aws
- azure
- gcp
@@ -5751,7 +5871,6 @@ paths:
- m365
- mongodbatlas
- oraclecloud
- - alibabacloud
description: |-
* `aws` - AWS
* `azure` - Azure
@@ -5769,8 +5888,9 @@ paths:
type: array
items:
type: string
- x-spec-enum-id: eca8c51e6bd28935
+ x-spec-enum-id: 684bf4173d2b754f
enum:
+ - alibabacloud
- aws
- azure
- gcp
@@ -5780,7 +5900,6 @@ paths:
- m365
- mongodbatlas
- oraclecloud
- - alibabacloud
description: |-
Multiple values may be separated by commas.
@@ -6563,8 +6682,9 @@ paths:
name: filter[provider]
schema:
type: string
- x-spec-enum-id: eca8c51e6bd28935
+ x-spec-enum-id: 684bf4173d2b754f
enum:
+ - alibabacloud
- aws
- azure
- gcp
@@ -6574,7 +6694,6 @@ paths:
- m365
- mongodbatlas
- oraclecloud
- - alibabacloud
description: |-
* `aws` - AWS
* `azure` - Azure
@@ -6592,8 +6711,9 @@ paths:
type: array
items:
type: string
- x-spec-enum-id: eca8c51e6bd28935
+ x-spec-enum-id: 684bf4173d2b754f
enum:
+ - alibabacloud
- aws
- azure
- gcp
@@ -6603,7 +6723,6 @@ paths:
- m365
- mongodbatlas
- oraclecloud
- - alibabacloud
description: |-
Multiple values may be separated by commas.
@@ -6623,8 +6742,9 @@ paths:
name: filter[provider_type]
schema:
type: string
- x-spec-enum-id: eca8c51e6bd28935
+ x-spec-enum-id: 684bf4173d2b754f
enum:
+ - alibabacloud
- aws
- azure
- gcp
@@ -6634,7 +6754,6 @@ paths:
- m365
- mongodbatlas
- oraclecloud
- - alibabacloud
description: |-
* `aws` - AWS
* `azure` - Azure
@@ -6652,8 +6771,9 @@ paths:
type: array
items:
type: string
- x-spec-enum-id: eca8c51e6bd28935
+ x-spec-enum-id: 684bf4173d2b754f
enum:
+ - alibabacloud
- aws
- azure
- gcp
@@ -6663,7 +6783,6 @@ paths:
- m365
- mongodbatlas
- oraclecloud
- - alibabacloud
description: |-
Multiple values may be separated by commas.
@@ -7277,8 +7396,9 @@ paths:
name: filter[provider_type]
schema:
type: string
- x-spec-enum-id: eca8c51e6bd28935
+ x-spec-enum-id: 684bf4173d2b754f
enum:
+ - alibabacloud
- aws
- azure
- gcp
@@ -7288,7 +7408,6 @@ paths:
- m365
- mongodbatlas
- oraclecloud
- - alibabacloud
description: |-
* `aws` - AWS
* `azure` - Azure
@@ -7306,8 +7425,9 @@ paths:
type: array
items:
type: string
- x-spec-enum-id: eca8c51e6bd28935
+ x-spec-enum-id: 684bf4173d2b754f
enum:
+ - alibabacloud
- aws
- azure
- gcp
@@ -7317,7 +7437,6 @@ paths:
- m365
- mongodbatlas
- oraclecloud
- - alibabacloud
description: |-
Multiple values may be separated by commas.
@@ -7664,8 +7783,9 @@ paths:
name: filter[provider_type]
schema:
type: string
- x-spec-enum-id: eca8c51e6bd28935
+ x-spec-enum-id: 684bf4173d2b754f
enum:
+ - alibabacloud
- aws
- azure
- gcp
@@ -7675,7 +7795,6 @@ paths:
- m365
- mongodbatlas
- oraclecloud
- - alibabacloud
description: |-
* `aws` - AWS
* `azure` - Azure
@@ -7693,8 +7812,9 @@ paths:
type: array
items:
type: string
- x-spec-enum-id: eca8c51e6bd28935
+ x-spec-enum-id: 684bf4173d2b754f
enum:
+ - alibabacloud
- aws
- azure
- gcp
@@ -7704,7 +7824,6 @@ paths:
- m365
- mongodbatlas
- oraclecloud
- - alibabacloud
description: |-
Multiple values may be separated by commas.
@@ -7946,8 +8065,9 @@ paths:
name: filter[provider_type]
schema:
type: string
- x-spec-enum-id: eca8c51e6bd28935
+ x-spec-enum-id: 684bf4173d2b754f
enum:
+ - alibabacloud
- aws
- azure
- gcp
@@ -7957,7 +8077,6 @@ paths:
- m365
- mongodbatlas
- oraclecloud
- - alibabacloud
description: |-
* `aws` - AWS
* `azure` - Azure
@@ -7975,8 +8094,9 @@ paths:
type: array
items:
type: string
- x-spec-enum-id: eca8c51e6bd28935
+ x-spec-enum-id: 684bf4173d2b754f
enum:
+ - alibabacloud
- aws
- azure
- gcp
@@ -7986,7 +8106,6 @@ paths:
- m365
- mongodbatlas
- oraclecloud
- - alibabacloud
description: |-
Multiple values may be separated by commas.
@@ -8234,8 +8353,9 @@ paths:
name: filter[provider_type]
schema:
type: string
- x-spec-enum-id: eca8c51e6bd28935
+ x-spec-enum-id: 684bf4173d2b754f
enum:
+ - alibabacloud
- aws
- azure
- gcp
@@ -8245,7 +8365,6 @@ paths:
- m365
- mongodbatlas
- oraclecloud
- - alibabacloud
description: |-
* `aws` - AWS
* `azure` - Azure
@@ -8263,8 +8382,9 @@ paths:
type: array
items:
type: string
- x-spec-enum-id: eca8c51e6bd28935
+ x-spec-enum-id: 684bf4173d2b754f
enum:
+ - alibabacloud
- aws
- azure
- gcp
@@ -8274,7 +8394,6 @@ paths:
- m365
- mongodbatlas
- oraclecloud
- - alibabacloud
description: |-
Multiple values may be separated by commas.
@@ -9085,8 +9204,9 @@ paths:
name: filter[provider_type]
schema:
type: string
- x-spec-enum-id: eca8c51e6bd28935
+ x-spec-enum-id: 684bf4173d2b754f
enum:
+ - alibabacloud
- aws
- azure
- gcp
@@ -9096,7 +9216,6 @@ paths:
- m365
- mongodbatlas
- oraclecloud
- - alibabacloud
description: |-
* `aws` - AWS
* `azure` - Azure
@@ -9114,8 +9233,9 @@ paths:
type: array
items:
type: string
- x-spec-enum-id: eca8c51e6bd28935
+ x-spec-enum-id: 684bf4173d2b754f
enum:
+ - alibabacloud
- aws
- azure
- gcp
@@ -9125,7 +9245,6 @@ paths:
- m365
- mongodbatlas
- oraclecloud
- - alibabacloud
description: |-
Multiple values may be separated by commas.
@@ -11417,6 +11536,50 @@ components:
type: string
required:
- regions
+ ComplianceWatchlistOverview:
+ type: object
+ required:
+ - type
+ - id
+ additionalProperties: false
+ properties:
+ type:
+ type: string
+ description: The [type](https://jsonapi.org/format/#document-resource-object-identification)
+ member is used to describe resource objects that share common attributes
+ and relationships.
+ enum:
+ - compliance-watchlist-overviews
+ id: {}
+ attributes:
+ type: object
+ properties:
+ id:
+ type: string
+ compliance_id:
+ type: string
+ requirements_passed:
+ type: integer
+ requirements_failed:
+ type: integer
+ requirements_manual:
+ type: integer
+ total_requirements:
+ type: integer
+ required:
+ - id
+ - compliance_id
+ - requirements_passed
+ - requirements_failed
+ - requirements_manual
+ - total_requirements
+ ComplianceWatchlistOverviewResponse:
+ type: object
+ properties:
+ data:
+ $ref: '#/components/schemas/ComplianceWatchlistOverview'
+ required:
+ - data
Finding:
type: object
required:
@@ -15637,6 +15800,44 @@ components:
required:
- atlas_public_key
- atlas_private_key
+ - type: object
+ title: Alibaba Cloud Static Credentials
+ properties:
+ access_key_id:
+ type: string
+ description: The Alibaba Cloud access key ID for authentication.
+ access_key_secret:
+ type: string
+ description: The Alibaba Cloud access key secret for authentication.
+ security_token:
+ type: string
+ description: The STS security token for temporary credentials
+ (optional).
+ required:
+ - access_key_id
+ - access_key_secret
+ - type: object
+ title: Alibaba Cloud RAM Role Assumption
+ properties:
+ role_arn:
+ type: string
+ description: The ARN of the RAM role to assume (e.g., acs:ram::1234567890123456:role/ProwlerRole).
+ access_key_id:
+ type: string
+ description: The Alibaba Cloud access key ID of the RAM user
+ that will assume the role.
+ access_key_secret:
+ type: string
+ description: The Alibaba Cloud access key secret of the RAM
+ user that will assume the role.
+ role_session_name:
+ type: string
+ description: An identifier for the role session (optional,
+ defaults to 'ProwlerSession').
+ required:
+ - role_arn
+ - access_key_id
+ - access_key_secret
writeOnly: true
required:
- secret
@@ -16647,7 +16848,7 @@ components:
* `iac` - IaC
* `oraclecloud` - Oracle Cloud Infrastructure
* `alibabacloud` - Alibaba Cloud
- x-spec-enum-id: eca8c51e6bd28935
+ x-spec-enum-id: 684bf4173d2b754f
uid:
type: string
title: Unique identifier for the provider, set by the provider
@@ -16764,7 +16965,7 @@ components:
- oraclecloud
- alibabacloud
type: string
- x-spec-enum-id: eca8c51e6bd28935
+ x-spec-enum-id: 684bf4173d2b754f
description: |-
Type of provider to create.
@@ -16826,7 +17027,7 @@ components:
- oraclecloud
- alibabacloud
type: string
- x-spec-enum-id: eca8c51e6bd28935
+ x-spec-enum-id: 684bf4173d2b754f
description: |-
Type of provider to create.
@@ -17602,6 +17803,44 @@ components:
required:
- atlas_public_key
- atlas_private_key
+ - type: object
+ title: Alibaba Cloud Static Credentials
+ properties:
+ access_key_id:
+ type: string
+ description: The Alibaba Cloud access key ID for authentication.
+ access_key_secret:
+ type: string
+ description: The Alibaba Cloud access key secret for authentication.
+ security_token:
+ type: string
+ description: The STS security token for temporary credentials
+ (optional).
+ required:
+ - access_key_id
+ - access_key_secret
+ - type: object
+ title: Alibaba Cloud RAM Role Assumption
+ properties:
+ role_arn:
+ type: string
+ description: The ARN of the RAM role to assume (e.g., acs:ram::1234567890123456:role/ProwlerRole).
+ access_key_id:
+ type: string
+ description: The Alibaba Cloud access key ID of the RAM user that
+ will assume the role.
+ access_key_secret:
+ type: string
+ description: The Alibaba Cloud access key secret of the RAM user
+ that will assume the role.
+ role_session_name:
+ type: string
+ description: An identifier for the role session (optional, defaults
+ to 'ProwlerSession').
+ required:
+ - role_arn
+ - access_key_id
+ - access_key_secret
writeOnly: true
required:
- secret_type
@@ -17927,6 +18166,44 @@ components:
required:
- atlas_public_key
- atlas_private_key
+ - type: object
+ title: Alibaba Cloud Static Credentials
+ properties:
+ access_key_id:
+ type: string
+ description: The Alibaba Cloud access key ID for authentication.
+ access_key_secret:
+ type: string
+ description: The Alibaba Cloud access key secret for authentication.
+ security_token:
+ type: string
+ description: The STS security token for temporary credentials
+ (optional).
+ required:
+ - access_key_id
+ - access_key_secret
+ - type: object
+ title: Alibaba Cloud RAM Role Assumption
+ properties:
+ role_arn:
+ type: string
+ description: The ARN of the RAM role to assume (e.g., acs:ram::1234567890123456:role/ProwlerRole).
+ access_key_id:
+ type: string
+ description: The Alibaba Cloud access key ID of the RAM user
+ that will assume the role.
+ access_key_secret:
+ type: string
+ description: The Alibaba Cloud access key secret of the RAM
+ user that will assume the role.
+ role_session_name:
+ type: string
+ description: An identifier for the role session (optional,
+ defaults to 'ProwlerSession').
+ required:
+ - role_arn
+ - access_key_id
+ - access_key_secret
writeOnly: true
required:
- secret_type
@@ -18266,6 +18543,44 @@ components:
required:
- atlas_public_key
- atlas_private_key
+ - type: object
+ title: Alibaba Cloud Static Credentials
+ properties:
+ access_key_id:
+ type: string
+ description: The Alibaba Cloud access key ID for authentication.
+ access_key_secret:
+ type: string
+ description: The Alibaba Cloud access key secret for authentication.
+ security_token:
+ type: string
+ description: The STS security token for temporary credentials
+ (optional).
+ required:
+ - access_key_id
+ - access_key_secret
+ - type: object
+ title: Alibaba Cloud RAM Role Assumption
+ properties:
+ role_arn:
+ type: string
+ description: The ARN of the RAM role to assume (e.g., acs:ram::1234567890123456:role/ProwlerRole).
+ access_key_id:
+ type: string
+ description: The Alibaba Cloud access key ID of the RAM user that
+ will assume the role.
+ access_key_secret:
+ type: string
+ description: The Alibaba Cloud access key secret of the RAM user
+ that will assume the role.
+ role_session_name:
+ type: string
+ description: An identifier for the role session (optional, defaults
+ to 'ProwlerSession').
+ required:
+ - role_arn
+ - access_key_id
+ - access_key_secret
writeOnly: true
required:
- secret
diff --git a/api/src/backend/api/tests/test_models.py b/api/src/backend/api/tests/test_models.py
index 79d405d4a7..13878794b1 100644
--- a/api/src/backend/api/tests/test_models.py
+++ b/api/src/backend/api/tests/test_models.py
@@ -1,9 +1,21 @@
+from datetime import datetime, timezone
+
import pytest
from allauth.socialaccount.models import SocialApp
from django.core.exceptions import ValidationError
+from django.db import IntegrityError
from api.db_router import MainRouter
-from api.models import Resource, ResourceTag, SAMLConfiguration, SAMLDomainIndex
+from api.models import (
+ ProviderComplianceScore,
+ Resource,
+ ResourceTag,
+ SAMLConfiguration,
+ SAMLDomainIndex,
+ StateChoices,
+ StatusChoices,
+ TenantComplianceSummary,
+)
@pytest.mark.django_db
@@ -324,3 +336,159 @@ class TestSAMLConfigurationModel:
errors = exc_info.value.message_dict
assert "metadata_xml" in errors
assert "There is a problem with your metadata." in errors["metadata_xml"][0]
+
+
+@pytest.mark.django_db
+class TestProviderComplianceScoreModel:
+ def test_create_provider_compliance_score(self, providers_fixture, scans_fixture):
+ provider = providers_fixture[0]
+ scan = scans_fixture[0]
+ scan.completed_at = datetime.now(timezone.utc)
+ scan.save()
+
+ score = ProviderComplianceScore.objects.create(
+ tenant_id=provider.tenant_id,
+ provider=provider,
+ scan=scan,
+ compliance_id="aws_cis_2.0",
+ requirement_id="req_1",
+ requirement_status=StatusChoices.PASS,
+ scan_completed_at=scan.completed_at,
+ )
+
+ assert score.compliance_id == "aws_cis_2.0"
+ assert score.requirement_id == "req_1"
+ assert score.requirement_status == StatusChoices.PASS
+
+ def test_unique_constraint_per_provider_compliance_requirement(
+ self, providers_fixture, scans_fixture
+ ):
+ provider = providers_fixture[0]
+ scan = scans_fixture[0]
+ scan.completed_at = datetime.now(timezone.utc)
+ scan.save()
+
+ ProviderComplianceScore.objects.create(
+ tenant_id=provider.tenant_id,
+ provider=provider,
+ scan=scan,
+ compliance_id="aws_cis_2.0",
+ requirement_id="req_1",
+ requirement_status=StatusChoices.PASS,
+ scan_completed_at=scan.completed_at,
+ )
+
+ with pytest.raises(IntegrityError):
+ ProviderComplianceScore.objects.create(
+ tenant_id=provider.tenant_id,
+ provider=provider,
+ scan=scan,
+ compliance_id="aws_cis_2.0",
+ requirement_id="req_1",
+ requirement_status=StatusChoices.FAIL,
+ scan_completed_at=scan.completed_at,
+ )
+
+ def test_different_providers_same_requirement_allowed(
+ self, providers_fixture, scans_fixture
+ ):
+ provider1, provider2, *_ = providers_fixture
+ scan1 = scans_fixture[0]
+ scan1.completed_at = datetime.now(timezone.utc)
+ scan1.save()
+
+ scan2 = scans_fixture[2]
+ scan2.state = StateChoices.COMPLETED
+ scan2.completed_at = datetime.now(timezone.utc)
+ scan2.save()
+
+ score1 = ProviderComplianceScore.objects.create(
+ tenant_id=provider1.tenant_id,
+ provider=provider1,
+ scan=scan1,
+ compliance_id="aws_cis_2.0",
+ requirement_id="req_1",
+ requirement_status=StatusChoices.PASS,
+ scan_completed_at=scan1.completed_at,
+ )
+
+ score2 = ProviderComplianceScore.objects.create(
+ tenant_id=provider2.tenant_id,
+ provider=provider2,
+ scan=scan2,
+ compliance_id="aws_cis_2.0",
+ requirement_id="req_1",
+ requirement_status=StatusChoices.FAIL,
+ scan_completed_at=scan2.completed_at,
+ )
+
+ assert score1.id != score2.id
+ assert score1.requirement_status != score2.requirement_status
+
+
+@pytest.mark.django_db
+class TestTenantComplianceSummaryModel:
+ def test_create_tenant_compliance_summary(self, tenants_fixture):
+ tenant = tenants_fixture[0]
+
+ summary = TenantComplianceSummary.objects.create(
+ tenant_id=tenant.id,
+ compliance_id="aws_cis_2.0",
+ requirements_passed=5,
+ requirements_failed=2,
+ requirements_manual=1,
+ total_requirements=8,
+ )
+
+ assert summary.compliance_id == "aws_cis_2.0"
+ assert summary.requirements_passed == 5
+ assert summary.requirements_failed == 2
+ assert summary.requirements_manual == 1
+ assert summary.total_requirements == 8
+ assert summary.updated_at is not None
+
+ def test_unique_constraint_per_tenant_compliance(self, tenants_fixture):
+ tenant = tenants_fixture[0]
+
+ TenantComplianceSummary.objects.create(
+ tenant_id=tenant.id,
+ compliance_id="aws_cis_2.0",
+ requirements_passed=5,
+ requirements_failed=2,
+ requirements_manual=1,
+ total_requirements=8,
+ )
+
+ with pytest.raises(IntegrityError):
+ TenantComplianceSummary.objects.create(
+ tenant_id=tenant.id,
+ compliance_id="aws_cis_2.0",
+ requirements_passed=3,
+ requirements_failed=4,
+ requirements_manual=1,
+ total_requirements=8,
+ )
+
+ def test_different_tenants_same_compliance_allowed(self, tenants_fixture):
+ tenant1, tenant2, *_ = tenants_fixture
+
+ summary1 = TenantComplianceSummary.objects.create(
+ tenant_id=tenant1.id,
+ compliance_id="aws_cis_2.0",
+ requirements_passed=5,
+ requirements_failed=2,
+ requirements_manual=1,
+ total_requirements=8,
+ )
+
+ summary2 = TenantComplianceSummary.objects.create(
+ tenant_id=tenant2.id,
+ compliance_id="aws_cis_2.0",
+ requirements_passed=3,
+ requirements_failed=4,
+ requirements_manual=1,
+ total_requirements=8,
+ )
+
+ assert summary1.id != summary2.id
+ assert summary1.requirements_passed != summary2.requirements_passed
diff --git a/api/src/backend/api/tests/test_views.py b/api/src/backend/api/tests/test_views.py
index 6babb92fc8..f2a0c299f2 100644
--- a/api/src/backend/api/tests/test_views.py
+++ b/api/src/backend/api/tests/test_views.py
@@ -4110,6 +4110,37 @@ class TestFindingViewSet:
assert response.status_code == status.HTTP_200_OK
assert len(response.json()["data"]) == 2
+ def test_finding_filter_by_provider_id_alias(
+ self, authenticated_client, findings_fixture
+ ):
+ """Test that provider_id filter alias works identically to provider filter."""
+ response = authenticated_client.get(
+ reverse("finding-list"),
+ {
+ "filter[provider_id]": findings_fixture[0].scan.provider.id,
+ "filter[inserted_at]": TODAY,
+ },
+ )
+ assert response.status_code == status.HTTP_200_OK
+ assert len(response.json()["data"]) == 2
+
+ def test_finding_filter_by_provider_id_in_alias(
+ self, authenticated_client, findings_fixture
+ ):
+ """Test that provider_id__in filter alias works identically to provider__in filter."""
+ response = authenticated_client.get(
+ reverse("finding-list"),
+ {
+ "filter[provider_id__in]": [
+ findings_fixture[0].scan.provider.id,
+ findings_fixture[1].scan.provider.id,
+ ],
+ "filter[inserted_at]": TODAY,
+ },
+ )
+ assert response.status_code == status.HTTP_200_OK
+ assert len(response.json()["data"]) == 2
+
@pytest.mark.parametrize(
"filter_name",
(
@@ -4331,6 +4362,28 @@ class TestFindingViewSet:
== latest_scan_finding.status
)
+ def test_findings_latest_filter_by_provider_id_alias(
+ self, authenticated_client, latest_scan_finding
+ ):
+ """Test that provider_id filter alias works on latest findings endpoint."""
+ response = authenticated_client.get(
+ reverse("finding-latest"),
+ {"filter[provider_id]": latest_scan_finding.scan.provider.id},
+ )
+ assert response.status_code == status.HTTP_200_OK
+ assert len(response.json()["data"]) == 1
+
+ def test_findings_latest_filter_by_provider_id_in_alias(
+ self, authenticated_client, latest_scan_finding
+ ):
+ """Test that provider_id__in filter alias works on latest findings endpoint."""
+ response = authenticated_client.get(
+ reverse("finding-latest"),
+ {"filter[provider_id__in]": str(latest_scan_finding.scan.provider.id)},
+ )
+ assert response.status_code == status.HTTP_200_OK
+ assert len(response.json()["data"]) == 1
+
def test_findings_metadata_latest(self, authenticated_client, latest_scan_finding):
response = authenticated_client.get(
reverse("finding-metadata_latest"),
@@ -7956,6 +8009,97 @@ class TestOverviewViewSet:
assert data[0]["attributes"]["failed_findings"] == 13
assert data[0]["attributes"]["new_failed_findings"] == 5
+ def test_compliance_watchlist_no_filters_uses_tenant_summary(
+ self, authenticated_client, tenant_compliance_summary_fixture
+ ):
+ response = authenticated_client.get(reverse("overview-compliance-watchlist"))
+ assert response.status_code == status.HTTP_200_OK
+ data = response.json()["data"]
+
+ assert len(data) == 2
+
+ by_id = {item["id"]: item["attributes"] for item in data}
+ assert "aws_cis_2.0" in by_id
+ assert by_id["aws_cis_2.0"]["requirements_passed"] == 1
+ assert by_id["aws_cis_2.0"]["requirements_failed"] == 2
+ assert by_id["aws_cis_2.0"]["requirements_manual"] == 1
+ assert by_id["aws_cis_2.0"]["total_requirements"] == 4
+
+ assert "gdpr_aws" in by_id
+ assert by_id["gdpr_aws"]["requirements_passed"] == 5
+ assert by_id["gdpr_aws"]["requirements_failed"] == 0
+ assert by_id["gdpr_aws"]["total_requirements"] == 7
+
+ def test_compliance_watchlist_with_provider_filter_uses_provider_scores(
+ self,
+ authenticated_client,
+ provider_compliance_scores_fixture,
+ providers_fixture,
+ ):
+ provider1 = providers_fixture[0]
+ url = f"{reverse('overview-compliance-watchlist')}?filter[provider_id]={provider1.id}"
+ response = authenticated_client.get(url)
+ assert response.status_code == status.HTTP_200_OK
+ data = response.json()["data"]
+
+ assert len(data) == 2
+ by_id = {item["id"]: item["attributes"] for item in data}
+
+ assert by_id["aws_cis_2.0"]["requirements_passed"] == 1
+ assert by_id["aws_cis_2.0"]["requirements_failed"] == 1
+ assert by_id["aws_cis_2.0"]["requirements_manual"] == 1
+ assert by_id["aws_cis_2.0"]["total_requirements"] == 3
+
+ def test_compliance_watchlist_fail_dominant_logic(
+ self, authenticated_client, provider_compliance_scores_fixture
+ ):
+ response = authenticated_client.get(
+ f"{reverse('overview-compliance-watchlist')}?filter[provider_type]=aws"
+ )
+ assert response.status_code == status.HTTP_200_OK
+ data = response.json()["data"]
+
+ by_id = {item["id"]: item["attributes"] for item in data}
+ aws_cis = by_id["aws_cis_2.0"]
+
+ assert aws_cis["requirements_failed"] == 2
+ assert aws_cis["requirements_passed"] == 0
+ assert aws_cis["requirements_manual"] == 1
+ assert aws_cis["total_requirements"] == 3
+
+ def test_compliance_watchlist_provider_id_in_filter(
+ self,
+ authenticated_client,
+ provider_compliance_scores_fixture,
+ providers_fixture,
+ ):
+ provider1, provider2, *_ = providers_fixture
+ url = (
+ f"{reverse('overview-compliance-watchlist')}"
+ f"?filter[provider_id__in]={provider1.id},{provider2.id}"
+ )
+ response = authenticated_client.get(url)
+ assert response.status_code == status.HTTP_200_OK
+ data = response.json()["data"]
+ assert len(data) >= 1
+
+ def test_compliance_watchlist_empty_result(self, authenticated_client):
+ response = authenticated_client.get(reverse("overview-compliance-watchlist"))
+ assert response.status_code == status.HTTP_200_OK
+ data = response.json()["data"]
+ assert data == []
+
+ @pytest.mark.parametrize(
+ "invalid_provider_type",
+ ["invalid", "not_a_provider", "AWS", "awss"],
+ )
+ def test_compliance_watchlist_invalid_provider_type_filter(
+ self, authenticated_client, invalid_provider_type
+ ):
+ url = f"{reverse('overview-compliance-watchlist')}?filter[provider_type]={invalid_provider_type}"
+ response = authenticated_client.get(url)
+ assert response.status_code == status.HTTP_400_BAD_REQUEST
+
@pytest.mark.django_db
class TestScheduleViewSet:
diff --git a/api/src/backend/api/v1/serializers.py b/api/src/backend/api/v1/serializers.py
index 25ca73d5ff..00c8c37dfb 100644
--- a/api/src/backend/api/v1/serializers.py
+++ b/api/src/backend/api/v1/serializers.py
@@ -2303,6 +2303,20 @@ class CategoryOverviewSerializer(BaseSerializerV1):
resource_name = "category-overviews"
+class ComplianceWatchlistOverviewSerializer(BaseSerializerV1):
+ """Serializer for compliance watchlist overview with FAIL-dominant aggregation."""
+
+ id = serializers.CharField(source="compliance_id")
+ compliance_id = serializers.CharField()
+ requirements_passed = serializers.IntegerField()
+ requirements_failed = serializers.IntegerField()
+ requirements_manual = serializers.IntegerField()
+ total_requirements = serializers.IntegerField()
+
+ class JSONAPIMeta:
+ resource_name = "compliance-watchlist-overviews"
+
+
class OverviewRegionSerializer(serializers.Serializer):
id = serializers.SerializerMethodField()
provider_type = serializers.CharField()
diff --git a/api/src/backend/api/v1/views.py b/api/src/backend/api/v1/views.py
index 54a33e28fa..ffdfc1005e 100644
--- a/api/src/backend/api/v1/views.py
+++ b/api/src/backend/api/v1/views.py
@@ -101,6 +101,7 @@ from api.filters import (
AttackSurfaceOverviewFilter,
CategoryOverviewFilter,
ComplianceOverviewFilter,
+ ComplianceWatchlistFilter,
CustomDjangoFilterBackend,
DailySeveritySummaryFilter,
FindingFilter,
@@ -144,6 +145,7 @@ from api.models import (
MuteRule,
Processor,
Provider,
+ ProviderComplianceScore,
ProviderGroup,
ProviderGroupMembership,
ProviderSecret,
@@ -163,6 +165,7 @@ from api.models import (
StateChoices,
Task,
TenantAPIKey,
+ TenantComplianceSummary,
ThreatScoreSnapshot,
User,
UserRoleRelationship,
@@ -185,6 +188,7 @@ from api.v1.serializers import (
ComplianceOverviewDetailThreatscoreSerializer,
ComplianceOverviewMetadataSerializer,
ComplianceOverviewSerializer,
+ ComplianceWatchlistOverviewSerializer,
FindingDynamicFilterSerializer,
FindingMetadataSerializer,
FindingSerializer,
@@ -4142,6 +4146,8 @@ class OverviewViewSet(BaseRLSViewSet):
return AttackSurfaceOverviewSerializer
elif self.action == "categories":
return CategoryOverviewSerializer
+ elif self.action == "compliance_watchlist":
+ return ComplianceWatchlistOverviewSerializer
return super().get_serializer_class()
def get_filterset_class(self):
@@ -4157,6 +4163,8 @@ class OverviewViewSet(BaseRLSViewSet):
return CategoryOverviewFilter
elif self.action == "attack_surface":
return AttackSurfaceOverviewFilter
+ elif self.action == "compliance_watchlist":
+ return ComplianceWatchlistFilter
return None
def filter_queryset(self, queryset):
@@ -4240,6 +4248,8 @@ class OverviewViewSet(BaseRLSViewSet):
self.request.query_params, exclude_keys=set(exclude_keys or [])
)
filterset = filterset_class(normalized_params, queryset=queryset)
+ if not filterset.is_valid():
+ raise ValidationError(filterset.errors)
return filterset.qs
def _latest_scan_ids_for_allowed_providers(self, tenant_id, provider_filters=None):
@@ -4256,9 +4266,10 @@ class OverviewViewSet(BaseRLSViewSet):
)
def _extract_provider_filters_from_params(self):
- """Extract provider filters from query params to apply on Scan queryset."""
+ """Extract and validate provider filters from query params."""
params = self.request.query_params
filters = {}
+ valid_provider_types = {c[0] for c in Provider.ProviderChoices.choices}
provider_id = params.get("filter[provider_id]")
if provider_id:
@@ -4270,11 +4281,21 @@ class OverviewViewSet(BaseRLSViewSet):
provider_type = params.get("filter[provider_type]")
if provider_type:
+ if provider_type not in valid_provider_types:
+ raise ValidationError(
+ {"provider_type": f"Invalid choice: {provider_type}"}
+ )
filters["provider__provider"] = provider_type
provider_type_in = params.get("filter[provider_type__in]")
if provider_type_in:
- filters["provider__provider__in"] = provider_type_in.split(",")
+ types = provider_type_in.split(",")
+ invalid = [t for t in types if t not in valid_provider_types]
+ if invalid:
+ raise ValidationError(
+ {"provider_type__in": f"Invalid choices: {', '.join(invalid)}"}
+ )
+ filters["provider__provider__in"] = types
return filters
@@ -4984,6 +5005,92 @@ class OverviewViewSet(BaseRLSViewSet):
status=status.HTTP_200_OK,
)
+ @action(
+ detail=False,
+ methods=["get"],
+ url_name="compliance-watchlist",
+ url_path="compliance-watchlist",
+ )
+ def compliance_watchlist(self, request):
+ """
+ Get compliance watchlist overview with FAIL-dominant aggregation.
+
+ Without filters: uses pre-aggregated TenantComplianceSummary (~70 rows).
+ With provider filters: queries ProviderComplianceScore with FAIL-dominant logic.
+ """
+ tenant_id = request.tenant_id
+ rbac_filter = self._get_provider_filter()
+ query_params = request.query_params
+
+ has_provider_filter = any(
+ key.startswith("filter[provider") for key in query_params.keys()
+ )
+ has_rbac_restriction = bool(rbac_filter)
+
+ if not has_provider_filter and not has_rbac_restriction:
+ response_data = list(
+ TenantComplianceSummary.objects.filter(tenant_id=tenant_id)
+ .values(
+ "compliance_id",
+ "requirements_passed",
+ "requirements_failed",
+ "requirements_manual",
+ "total_requirements",
+ )
+ .order_by("compliance_id")
+ )
+ else:
+ base_queryset = ProviderComplianceScore.objects.filter(
+ tenant_id=tenant_id, **rbac_filter
+ )
+
+ filtered_queryset = self._apply_filterset(
+ base_queryset, ComplianceWatchlistFilter
+ )
+
+ aggregation = (
+ filtered_queryset.values("compliance_id", "requirement_id")
+ .annotate(
+ has_fail=Sum(
+ Case(When(requirement_status="FAIL", then=1), default=0)
+ ),
+ has_manual=Sum(
+ Case(When(requirement_status="MANUAL", then=1), default=0)
+ ),
+ )
+ .values("compliance_id", "requirement_id", "has_fail", "has_manual")
+ )
+
+ compliance_data = defaultdict(
+ lambda: {
+ "requirements_passed": 0,
+ "requirements_failed": 0,
+ "requirements_manual": 0,
+ "total_requirements": 0,
+ }
+ )
+
+ for row in aggregation:
+ cid = row["compliance_id"]
+ compliance_data[cid]["total_requirements"] += 1
+
+ if row["has_fail"] and row["has_fail"] > 0:
+ compliance_data[cid]["requirements_failed"] += 1
+ elif row["has_manual"] and row["has_manual"] > 0:
+ compliance_data[cid]["requirements_manual"] += 1
+ else:
+ compliance_data[cid]["requirements_passed"] += 1
+
+ response_data = [
+ {"compliance_id": cid, **data}
+ for cid, data in sorted(compliance_data.items())
+ ]
+
+ return Response(
+ self.get_serializer(response_data, many=True).data,
+ status=status.HTTP_200_OK,
+ )
+
@extend_schema(tags=["Schedule"])
@extend_schema_view(
diff --git a/api/src/backend/conftest.py b/api/src/backend/conftest.py
index 28432c59f2..8eefab551f 100644
--- a/api/src/backend/conftest.py
+++ b/api/src/backend/conftest.py
@@ -30,6 +30,7 @@ from api.models import (
MuteRule,
Processor,
Provider,
+ ProviderComplianceScore,
ProviderGroup,
ProviderSecret,
Resource,
@@ -45,6 +46,7 @@ from api.models import (
StatusChoices,
Task,
TenantAPIKey,
+ TenantComplianceSummary,
User,
UserRoleRelationship,
)
@@ -1631,6 +1633,108 @@ def get_authorization_header(access_token: str) -> dict:
return {"Authorization": f"Bearer {access_token}"}
+@pytest.fixture
+def provider_compliance_scores_fixture(
+ tenants_fixture, providers_fixture, scans_fixture
+):
+ """Create ProviderComplianceScore entries for compliance watchlist tests."""
+ tenant = tenants_fixture[0]
+ provider1, provider2, *_ = providers_fixture
+ scan1, _, scan3 = scans_fixture
+
+ scan1.completed_at = datetime.now(timezone.utc) - timedelta(hours=1)
+ scan1.save()
+ scan3.state = StateChoices.COMPLETED
+ scan3.completed_at = datetime.now(timezone.utc)
+ scan3.save()
+
+ scores = [
+ ProviderComplianceScore.objects.create(
+ tenant_id=tenant.id,
+ provider=provider1,
+ scan=scan1,
+ compliance_id="aws_cis_2.0",
+ requirement_id="req_1",
+ requirement_status=StatusChoices.PASS,
+ scan_completed_at=scan1.completed_at,
+ ),
+ ProviderComplianceScore.objects.create(
+ tenant_id=tenant.id,
+ provider=provider1,
+ scan=scan1,
+ compliance_id="aws_cis_2.0",
+ requirement_id="req_2",
+ requirement_status=StatusChoices.FAIL,
+ scan_completed_at=scan1.completed_at,
+ ),
+ ProviderComplianceScore.objects.create(
+ tenant_id=tenant.id,
+ provider=provider1,
+ scan=scan1,
+ compliance_id="aws_cis_2.0",
+ requirement_id="req_3",
+ requirement_status=StatusChoices.MANUAL,
+ scan_completed_at=scan1.completed_at,
+ ),
+ ProviderComplianceScore.objects.create(
+ tenant_id=tenant.id,
+ provider=provider2,
+ scan=scan3,
+ compliance_id="aws_cis_2.0",
+ requirement_id="req_1",
+ requirement_status=StatusChoices.FAIL,
+ scan_completed_at=scan3.completed_at,
+ ),
+ ProviderComplianceScore.objects.create(
+ tenant_id=tenant.id,
+ provider=provider2,
+ scan=scan3,
+ compliance_id="aws_cis_2.0",
+ requirement_id="req_2",
+ requirement_status=StatusChoices.PASS,
+ scan_completed_at=scan3.completed_at,
+ ),
+ ProviderComplianceScore.objects.create(
+ tenant_id=tenant.id,
+ provider=provider1,
+ scan=scan1,
+ compliance_id="gdpr_aws",
+ requirement_id="gdpr_req_1",
+ requirement_status=StatusChoices.PASS,
+ scan_completed_at=scan1.completed_at,
+ ),
+ ]
+
+ return scores
+
+
+@pytest.fixture
+def tenant_compliance_summary_fixture(tenants_fixture):
+ """Create TenantComplianceSummary entries for compliance watchlist tests."""
+ tenant = tenants_fixture[0]
+
+ summaries = [
+ TenantComplianceSummary.objects.create(
+ tenant_id=tenant.id,
+ compliance_id="aws_cis_2.0",
+ requirements_passed=1,
+ requirements_failed=2,
+ requirements_manual=1,
+ total_requirements=4,
+ ),
+ TenantComplianceSummary.objects.create(
+ tenant_id=tenant.id,
+ compliance_id="gdpr_aws",
+ requirements_passed=5,
+ requirements_failed=0,
+ requirements_manual=2,
+ total_requirements=7,
+ ),
+ ]
+
+ return summaries
+
+
def pytest_collection_modifyitems(items):
"""Ensure test_rbac.py is executed first."""
items.sort(key=lambda item: 0 if "test_rbac.py" in item.nodeid else 1)
diff --git a/api/src/backend/tasks/jobs/backfill.py b/api/src/backend/tasks/jobs/backfill.py
index 8571958448..851efe4e4a 100644
--- a/api/src/backend/tasks/jobs/backfill.py
+++ b/api/src/backend/tasks/jobs/backfill.py
@@ -1,17 +1,28 @@
from collections import defaultdict
from datetime import timedelta
+from celery.utils.log import get_task_logger
from django.db.models import Sum
from django.utils import timezone
+from tasks.jobs.queries import (
+ COMPLIANCE_UPSERT_PROVIDER_SCORE_SQL,
+ COMPLIANCE_UPSERT_TENANT_SUMMARY_ALL_SQL,
+)
from tasks.jobs.scan import aggregate_category_counts
-from api.db_router import READ_REPLICA_ALIAS
-from api.db_utils import rls_transaction
+from api.db_router import READ_REPLICA_ALIAS, MainRouter
+from api.db_utils import (
+ POSTGRES_TENANT_VAR,
+ SET_CONFIG_QUERY,
+ psycopg_connection,
+ rls_transaction,
+)
from api.models import (
ComplianceOverviewSummary,
ComplianceRequirementOverview,
DailySeveritySummary,
Finding,
+ ProviderComplianceScore,
Resource,
ResourceFindingMapping,
ResourceScanSummary,
@@ -21,6 +32,8 @@ from api.models import (
StateChoices,
)
+logger = get_task_logger(__name__)
+
def backfill_resource_scan_summaries(tenant_id: str, scan_id: str):
with rls_transaction(tenant_id, using=READ_REPLICA_ALIAS):
@@ -341,3 +354,114 @@ def backfill_scan_category_summaries(tenant_id: str, scan_id: str):
)
return {"status": "backfilled", "categories_count": len(category_counts)}
+
+
+def backfill_provider_compliance_scores(tenant_id: str) -> dict:
+ """
+ Backfill ProviderComplianceScore from latest completed scan per provider.
+
+ For each provider with completed scans, finds the most recent scan and
+ upserts compliance requirement statuses with FAIL-dominant aggregation.
+
+ Args:
+ tenant_id: Target tenant UUID
+
+ Returns:
+ dict: Statistics about the backfill operation
+ """
+ with rls_transaction(tenant_id, using=READ_REPLICA_ALIAS):
+ completed_scans = Scan.all_objects.filter(
+ tenant_id=tenant_id,
+ state=StateChoices.COMPLETED,
+ completed_at__isnull=False,
+ )
+ if not completed_scans.exists():
+ return {"status": "no completed scans"}
+
+ existing_providers = set(
+ ProviderComplianceScore.objects.filter(tenant_id=tenant_id)
+ .values_list("provider_id", flat=True)
+ .distinct()
+ )
+
+ if existing_providers:
+ completed_scans = completed_scans.exclude(
+ provider_id__in=existing_providers
+ )
+
+ scan_info = list(
+ completed_scans.order_by("provider_id", "-completed_at")
+ .distinct("provider_id")
+ .values("id", "provider_id", "completed_at")
+ )
+
+ if not scan_info:
+ return {"status": "no scans to process"}
+
+ total_upserted = 0
+ providers_processed = 0
+ providers_skipped = 0
+
+ for scan in scan_info:
+ provider_id = scan["provider_id"]
+
+ scan_id = scan["id"]
+
+ try:
+ with psycopg_connection(MainRouter.default_db) as connection:
+ connection.autocommit = False
+ try:
+ with connection.cursor() as cursor:
+ cursor.execute(
+ SET_CONFIG_QUERY, [POSTGRES_TENANT_VAR, tenant_id]
+ )
+ cursor.execute(
+ COMPLIANCE_UPSERT_PROVIDER_SCORE_SQL,
+ [tenant_id, str(scan_id)],
+ )
+ upserted = cursor.rowcount
+ connection.commit()
+ total_upserted += upserted
+ providers_processed += 1
+ except Exception:
+ connection.rollback()
+ raise
+ except Exception as e:
+ providers_skipped += 1
+ logger.exception(
+ "Error backfilling provider %s for tenant %s: %s",
+ provider_id,
+ tenant_id,
+ e,
+ )
+
+ # Recalculate tenant summary after all providers are backfilled
+ if providers_processed > 0:
+ with psycopg_connection(MainRouter.default_db) as connection:
+ connection.autocommit = False
+ try:
+ with connection.cursor() as cursor:
+ cursor.execute(SET_CONFIG_QUERY, [POSTGRES_TENANT_VAR, tenant_id])
+ # Advisory lock to prevent race conditions
+ cursor.execute(
+ "SELECT pg_advisory_xact_lock(hashtext(%s))", [tenant_id]
+ )
+ cursor.execute(
+ COMPLIANCE_UPSERT_TENANT_SUMMARY_ALL_SQL,
+ [tenant_id, tenant_id],
+ )
+ tenant_summary_count = cursor.rowcount
+ connection.commit()
+ except Exception:
+ connection.rollback()
+ raise
+ else:
+ tenant_summary_count = 0
+
+ return {
+ "status": "backfilled",
+ "providers_processed": providers_processed,
+ "providers_skipped": providers_skipped,
+ "total_upserted": total_upserted,
+ "tenant_summary_count": tenant_summary_count,
+ }
diff --git a/api/src/backend/tasks/jobs/queries.py b/api/src/backend/tasks/jobs/queries.py
new file mode 100644
index 0000000000..9fbe4901b2
--- /dev/null
+++ b/api/src/backend/tasks/jobs/queries.py
@@ -0,0 +1,134 @@
+"""
+Shared SQL queries for tasks.
+
+This module centralizes raw SQL queries used across multiple task modules
+to ensure consistency and maintainability.
+"""
+
+# =============================================================================
+# COMPLIANCE SCORE QUERIES
+# =============================================================================
+
+# Upsert provider compliance scores from a scan's compliance requirements.
+# Uses FAIL-dominant aggregation: FAIL > MANUAL > PASS
+# Parameters: [tenant_id, scan_id]
+COMPLIANCE_UPSERT_PROVIDER_SCORE_SQL = """
+ INSERT INTO provider_compliance_scores
+ (id, tenant_id, provider_id, scan_id, compliance_id, requirement_id,
+ requirement_status, scan_completed_at)
+ SELECT
+ gen_random_uuid(),
+ agg.tenant_id,
+ agg.provider_id,
+ agg.scan_id,
+ agg.compliance_id,
+ agg.requirement_id,
+ agg.requirement_status,
+ agg.completed_at
+ FROM (
+ SELECT DISTINCT ON (cro.compliance_id, cro.requirement_id)
+ cro.tenant_id,
+ s.provider_id,
+ cro.scan_id,
+ cro.compliance_id,
+ cro.requirement_id,
+ (CASE
+ WHEN bool_or(cro.requirement_status = 'FAIL')
+ OVER (PARTITION BY cro.compliance_id, cro.requirement_id) THEN 'FAIL'
+ WHEN bool_or(cro.requirement_status = 'MANUAL')
+ OVER (PARTITION BY cro.compliance_id, cro.requirement_id) THEN 'MANUAL'
+ ELSE 'PASS'
+ END)::status as requirement_status,
+ s.completed_at
+ FROM compliance_requirements_overviews cro
+ JOIN scans s ON s.id = cro.scan_id
+ WHERE cro.tenant_id = %s AND cro.scan_id = %s
+ ORDER BY cro.compliance_id, cro.requirement_id
+ ) agg
+ ON CONFLICT (tenant_id, provider_id, compliance_id, requirement_id)
+ DO UPDATE SET
+ requirement_status = EXCLUDED.requirement_status,
+ scan_id = EXCLUDED.scan_id,
+ scan_completed_at = EXCLUDED.scan_completed_at
+ WHERE EXCLUDED.scan_completed_at > provider_compliance_scores.scan_completed_at
+"""
+
+# Upsert tenant compliance summary for specific compliance IDs.
+# Aggregates across all providers with FAIL-dominant logic at requirement level.
+# Parameters: [tenant_id, tenant_id, compliance_ids_array]
+COMPLIANCE_UPSERT_TENANT_SUMMARY_SQL = """
+ INSERT INTO tenant_compliance_summaries
+ (id, tenant_id, compliance_id,
+ requirements_passed, requirements_failed, requirements_manual,
+ total_requirements, updated_at)
+ SELECT
+ gen_random_uuid(),
+ %s as tenant_id,
+ compliance_id,
+ COUNT(*) FILTER (WHERE req_status = 'PASS') as requirements_passed,
+ COUNT(*) FILTER (WHERE req_status = 'FAIL') as requirements_failed,
+ COUNT(*) FILTER (WHERE req_status = 'MANUAL') as requirements_manual,
+ COUNT(*) as total_requirements,
+ NOW() as updated_at
+ FROM (
+ SELECT
+ compliance_id,
+ requirement_id,
+ CASE
+ WHEN bool_or(requirement_status = 'FAIL') THEN 'FAIL'
+ WHEN bool_or(requirement_status = 'MANUAL') THEN 'MANUAL'
+ ELSE 'PASS'
+ END as req_status
+ FROM provider_compliance_scores
+ WHERE tenant_id = %s AND compliance_id = ANY(%s)
+ GROUP BY compliance_id, requirement_id
+ ) req_agg
+ GROUP BY compliance_id
+ ON CONFLICT (tenant_id, compliance_id)
+ DO UPDATE SET
+ requirements_passed = EXCLUDED.requirements_passed,
+ requirements_failed = EXCLUDED.requirements_failed,
+ requirements_manual = EXCLUDED.requirements_manual,
+ total_requirements = EXCLUDED.total_requirements,
+ updated_at = NOW()
+"""
+
+# Upsert tenant compliance summary for ALL compliance IDs in tenant.
+# Used by backfill when recalculating entire tenant summary.
+# Parameters: [tenant_id, tenant_id]
+COMPLIANCE_UPSERT_TENANT_SUMMARY_ALL_SQL = """
+ INSERT INTO tenant_compliance_summaries
+ (id, tenant_id, compliance_id,
+ requirements_passed, requirements_failed, requirements_manual,
+ total_requirements, updated_at)
+ SELECT
+ gen_random_uuid(),
+ %s as tenant_id,
+ compliance_id,
+ COUNT(*) FILTER (WHERE req_status = 'PASS') as requirements_passed,
+ COUNT(*) FILTER (WHERE req_status = 'FAIL') as requirements_failed,
+ COUNT(*) FILTER (WHERE req_status = 'MANUAL') as requirements_manual,
+ COUNT(*) as total_requirements,
+ NOW() as updated_at
+ FROM (
+ SELECT
+ compliance_id,
+ requirement_id,
+ CASE
+ WHEN bool_or(requirement_status = 'FAIL') THEN 'FAIL'
+ WHEN bool_or(requirement_status = 'MANUAL') THEN 'MANUAL'
+ ELSE 'PASS'
+ END as req_status
+ FROM provider_compliance_scores
+ WHERE tenant_id = %s
+ GROUP BY compliance_id, requirement_id
+ ) req_agg
+ GROUP BY compliance_id
+ ON CONFLICT (tenant_id, compliance_id)
+ DO UPDATE SET
+ requirements_passed = EXCLUDED.requirements_passed,
+ requirements_failed = EXCLUDED.requirements_failed,
+ requirements_manual = EXCLUDED.requirements_manual,
+ total_requirements = EXCLUDED.total_requirements,
+ updated_at = NOW()
+"""
diff --git a/api/src/backend/tasks/jobs/scan.py b/api/src/backend/tasks/jobs/scan.py
index 3b93dc3b84..9e40e2df03 100644
--- a/api/src/backend/tasks/jobs/scan.py
+++ b/api/src/backend/tasks/jobs/scan.py
@@ -14,6 +14,10 @@ from config.env import env
from config.settings.celery import CELERY_DEADLOCK_ATTEMPTS
from django.db import IntegrityError, OperationalError
from django.db.models import Case, Count, IntegerField, Prefetch, Q, Sum, When
+from tasks.jobs.queries import (
+ COMPLIANCE_UPSERT_PROVIDER_SCORE_SQL,
+ COMPLIANCE_UPSERT_TENANT_SUMMARY_SQL,
+)
from tasks.utils import CustomEncoder
from api.compliance import PROWLER_COMPLIANCE_OVERVIEW_TEMPLATE
@@ -1489,3 +1493,140 @@ def aggregate_daily_severity(tenant_id: str, scan_id: str):
"date": str(scan_date),
"severity_data": severity_data,
}
+
+
+def update_provider_compliance_scores(tenant_id: str, scan_id: str):
+ """
+ Update ProviderComplianceScore with requirement statuses from a completed scan.
+
+ Uses atomic SQL upsert with ON CONFLICT for concurrency safety. Only updates
+ if the new scan is more recent than existing data. Also cleans up stale
+ requirements that no longer exist in the new scan.
+
+ Reads from primary DB (not replica) to avoid replication lag issues since
+ this runs immediately after create_compliance_requirements_task.
+
+ Args:
+ tenant_id: Tenant that owns the scan.
+ scan_id: Scan UUID whose compliance data should be materialized.
+
+ Returns:
+ dict: Statistics about the upsert operation.
+ """
+ with rls_transaction(tenant_id):
+ scan = (
+ Scan.all_objects.filter(
+ tenant_id=tenant_id,
+ id=scan_id,
+ state=StateChoices.COMPLETED,
+ )
+ .select_related("provider")
+ .first()
+ )
+
+ if not scan:
+ logger.warning(
+ f"Scan {scan_id} not found or not completed for compliance score update"
+ )
+ return {"status": "skipped", "reason": "scan not completed"}
+
+ if not scan.completed_at:
+ logger.warning(f"Scan {scan_id} has no completed_at timestamp")
+ return {"status": "skipped", "reason": "no completed_at"}
+
+ provider_id = str(scan.provider_id)
+ scan_completed_at = scan.completed_at
+
+ delete_stale_sql = """
+ DELETE FROM provider_compliance_scores pcs
+ WHERE pcs.tenant_id = %s
+ AND pcs.provider_id = %s
+ AND pcs.scan_completed_at < %s
+ AND NOT EXISTS (
+ SELECT 1 FROM compliance_requirements_overviews cro
+ WHERE cro.tenant_id = pcs.tenant_id
+ AND cro.scan_id = %s
+ AND cro.compliance_id = pcs.compliance_id
+ AND cro.requirement_id = pcs.requirement_id
+ )
+ RETURNING compliance_id
+ """
+
+ compliance_ids_sql = """
+ SELECT DISTINCT compliance_id
+ FROM compliance_requirements_overviews
+ WHERE tenant_id = %s AND scan_id = %s
+ """
+
+ try:
+ with psycopg_connection(MainRouter.default_db) as connection:
+ connection.autocommit = False
+ try:
+ with connection.cursor() as cursor:
+ cursor.execute(SET_CONFIG_QUERY, [POSTGRES_TENANT_VAR, tenant_id])
+
+ # Update requirement-level scores per provider
+ cursor.execute(
+ COMPLIANCE_UPSERT_PROVIDER_SCORE_SQL, [tenant_id, scan_id]
+ )
+ upserted_count = cursor.rowcount
+
+ cursor.execute(compliance_ids_sql, [tenant_id, scan_id])
+ scan_rows = cursor.fetchall()
+ if not isinstance(scan_rows, (list, tuple)):
+ scan_rows = []
+ scan_compliance_ids = {row[0] for row in scan_rows}
+
+ cursor.execute(
+ delete_stale_sql,
+ [tenant_id, provider_id, scan_completed_at, scan_id],
+ )
+ deleted_rows = cursor.fetchall()
+ if not isinstance(deleted_rows, (list, tuple)):
+ deleted_rows = []
+ deleted_ids = {row[0] for row in deleted_rows}
+ stale_deleted = len(deleted_ids)
+
+ impacted_compliance_ids = sorted(scan_compliance_ids | deleted_ids)
+
+ if impacted_compliance_ids:
+ # Advisory lock on tenant to prevent race conditions when
+ # multiple scans complete simultaneously for the same tenant
+ cursor.execute(
+ "SELECT pg_advisory_xact_lock(hashtext(%s))", [tenant_id]
+ )
+
+ # Recalculate tenant-level summary (FAIL-dominant across all providers)
+ cursor.execute(
+ COMPLIANCE_UPSERT_TENANT_SUMMARY_SQL,
+ [tenant_id, tenant_id, impacted_compliance_ids],
+ )
+ tenant_summary_count = cursor.rowcount
+ else:
+ tenant_summary_count = 0
+
+ connection.commit()
+ except Exception:
+ connection.rollback()
+ raise
+
+ logger.info(
+ f"Provider compliance scores updated for scan {scan_id}: "
+ f"{upserted_count} upserted, {stale_deleted} stale deleted, "
+ f"{tenant_summary_count} tenant summaries upserted"
+ )
+
+ return {
+ "status": "completed",
+ "scan_id": str(scan_id),
+ "provider_id": provider_id,
+ "upserted": upserted_count,
+ "stale_deleted": stale_deleted,
+ "tenant_summary_count": tenant_summary_count,
+ }
+
+ except Exception as e:
+ logger.error(
+ f"Error updating provider compliance scores for scan {scan_id}: {e}"
+ )
+ raise
diff --git a/api/src/backend/tasks/tasks.py b/api/src/backend/tasks/tasks.py
index b4994fa41d..542f01ef65 100644
--- a/api/src/backend/tasks/tasks.py
+++ b/api/src/backend/tasks/tasks.py
@@ -11,6 +11,7 @@ from django_celery_beat.models import PeriodicTask
from tasks.jobs.backfill import (
backfill_compliance_summaries,
backfill_daily_severity_summaries,
+ backfill_provider_compliance_scores,
backfill_resource_scan_summaries,
backfill_scan_category_summaries,
)
@@ -44,6 +45,7 @@ from tasks.jobs.scan import (
aggregate_findings,
create_compliance_requirements,
perform_prowler_scan,
+ update_provider_compliance_scores,
)
from tasks.utils import batched, get_next_execution_datetime
@@ -122,9 +124,10 @@ def _perform_scan_complete_tasks(tenant_id: str, scan_id: str, provider_id: str)
scan_id (str): The ID of the scan that was performed.
provider_id (str): The primary key of the Provider instance that was scanned.
"""
- create_compliance_requirements_task.apply_async(
- kwargs={"tenant_id": tenant_id, "scan_id": scan_id}
- )
+ chain(
+ create_compliance_requirements_task.si(tenant_id=tenant_id, scan_id=scan_id),
+ update_provider_compliance_scores_task.si(tenant_id=tenant_id, scan_id=scan_id),
+ ).apply_async()
aggregate_attack_surface_task.apply_async(
kwargs={"tenant_id": tenant_id, "scan_id": scan_id}
)
@@ -610,6 +613,20 @@ def backfill_scan_category_summaries_task(tenant_id: str, scan_id: str):
return backfill_scan_category_summaries(tenant_id=tenant_id, scan_id=scan_id)
+@shared_task(name="backfill-provider-compliance-scores", queue="backfill")
+def backfill_provider_compliance_scores_task(tenant_id: str):
+ """
+ Backfill ProviderComplianceScore from latest completed scan per provider.
+
+ Used to populate the compliance watchlist materialized table for tenants
+ that had scans before the feature was deployed.
+
+ Args:
+ tenant_id: Target tenant UUID.
+ """
+ return backfill_provider_compliance_scores(tenant_id=tenant_id)
+
+
@shared_task(base=RLSTask, name="scan-compliance-overviews", queue="compliance")
@handle_provider_deletion
def create_compliance_requirements_task(tenant_id: str, scan_id: str):
@@ -643,6 +660,21 @@ def aggregate_attack_surface_task(tenant_id: str, scan_id: str):
return aggregate_attack_surface(tenant_id=tenant_id, scan_id=scan_id)
+@shared_task(name="scan-provider-compliance-scores", queue="compliance")
+def update_provider_compliance_scores_task(tenant_id: str, scan_id: str):
+ """
+ Update provider compliance scores from a completed scan.
+
+ This task materializes compliance requirement statuses into ProviderComplianceScore
+ for efficient watchlist queries. Uses atomic upsert with concurrency protection.
+
+ Args:
+ tenant_id (str): The tenant ID for which to update scores.
+ scan_id (str): The ID of the scan whose data should be materialized.
+ """
+ return update_provider_compliance_scores(tenant_id=tenant_id, scan_id=scan_id)
+
+
@shared_task(name="scan-daily-severity", queue="overview")
@handle_provider_deletion
def aggregate_daily_severity_task(tenant_id: str, scan_id: str):
diff --git a/api/src/backend/tasks/tests/test_backfill.py b/api/src/backend/tasks/tests/test_backfill.py
index b7a953e2e2..4c9780d101 100644
--- a/api/src/backend/tasks/tests/test_backfill.py
+++ b/api/src/backend/tasks/tests/test_backfill.py
@@ -1,8 +1,11 @@
+from datetime import datetime, timezone
+from unittest.mock import MagicMock, patch
from uuid import uuid4
import pytest
from tasks.jobs.backfill import (
backfill_compliance_summaries,
+ backfill_provider_compliance_scores,
backfill_resource_scan_summaries,
backfill_scan_category_summaries,
)
@@ -260,3 +263,62 @@ class TestBackfillScanCategorySummaries:
assert summary.total_findings == 1
assert summary.failed_findings == 1
assert summary.new_failed_findings == 1
+
+
+@pytest.mark.django_db
+class TestBackfillProviderComplianceScores:
+ def test_no_completed_scans(self, tenants_fixture):
+ tenant = tenants_fixture[2]
+ result = backfill_provider_compliance_scores(str(tenant.id))
+ assert result == {"status": "no completed scans"}
+
+ def test_no_scans_to_process(self, tenants_fixture, scans_fixture):
+ tenant = tenants_fixture[0]
+ scan = scans_fixture[0]
+ scan.completed_at = None
+ scan.save()
+
+ result = backfill_provider_compliance_scores(str(tenant.id))
+ assert result == {"status": "no completed scans"}
+
+ @patch("tasks.jobs.backfill.psycopg_connection")
+ def test_successful_backfill_executes_sql_queries(
+ self,
+ mock_psycopg_connection,
+ tenants_fixture,
+ scans_fixture,
+ settings,
+ ):
+ """Test successful backfill executes SQL queries and returns correct stats."""
+ settings.DATABASES.setdefault("admin", settings.DATABASES["default"])
+ tenant = tenants_fixture[0]
+ scan = scans_fixture[0]
+
+ # Set completed_at to make the scan eligible for backfill
+ scan.completed_at = datetime.now(timezone.utc)
+ scan.save()
+
+ connection = MagicMock()
+ cursor = MagicMock()
+ cursor_context = MagicMock()
+ cursor_context.__enter__.return_value = cursor
+ cursor_context.__exit__.return_value = False
+ connection.cursor.return_value = cursor_context
+ connection.__enter__.return_value = connection
+ connection.__exit__.return_value = False
+ connection.autocommit = True
+
+ context_manager = MagicMock()
+ context_manager.__enter__.return_value = connection
+ context_manager.__exit__.return_value = False
+ mock_psycopg_connection.return_value = context_manager
+
+ cursor.rowcount = 5
+
+ result = backfill_provider_compliance_scores(str(tenant.id))
+
+ assert result["status"] == "backfilled"
+ assert result["providers_processed"] == 1
+ assert result["providers_skipped"] == 0
+ assert result["total_upserted"] == 5
+ assert result["tenant_summary_count"] == 5
diff --git a/api/src/backend/tasks/tests/test_scan.py b/api/src/backend/tasks/tests/test_scan.py
index ec29dce14f..8902b17b54 100644
--- a/api/src/backend/tasks/tests/test_scan.py
+++ b/api/src/backend/tasks/tests/test_scan.py
@@ -24,6 +24,7 @@ from tasks.jobs.scan import (
aggregate_findings,
create_compliance_requirements,
perform_prowler_scan,
+ update_provider_compliance_scores,
)
from tasks.utils import CustomEncoder
@@ -4022,3 +4023,123 @@ class TestAggregateCategoryCounts:
assert len(cache) == 3
for cat in ["security", "compliance", "data-protection"]:
assert cache[(cat, "low")] == {"total": 1, "failed": 1, "new_failed": 1}
+
+
+@pytest.mark.django_db
+class TestUpdateProviderComplianceScores:
+ @patch("tasks.jobs.scan.psycopg_connection")
+ def test_update_provider_compliance_scores_basic(
+ self,
+ mock_psycopg_connection,
+ tenants_fixture,
+ scans_fixture,
+ settings,
+ ):
+ settings.DATABASES.setdefault("admin", settings.DATABASES["default"])
+ tenant = tenants_fixture[0]
+ scan = scans_fixture[0]
+ tenant_id = str(tenant.id)
+ scan_id = str(scan.id)
+
+ scan.state = StateChoices.COMPLETED
+ scan.completed_at = datetime.now(timezone.utc)
+ scan.save()
+
+ connection = MagicMock()
+ cursor = MagicMock()
+ cursor_context = MagicMock()
+ cursor_context.__enter__.return_value = cursor
+ cursor_context.__exit__.return_value = False
+ connection.cursor.return_value = cursor_context
+ connection.__enter__.return_value = connection
+ connection.__exit__.return_value = False
+ connection.autocommit = True
+
+ context_manager = MagicMock()
+ context_manager.__enter__.return_value = connection
+ context_manager.__exit__.return_value = False
+ mock_psycopg_connection.return_value = context_manager
+
+ cursor.rowcount = 2
+
+ result = update_provider_compliance_scores(tenant_id, scan_id)
+
+ assert result["status"] == "completed"
+ assert result["upserted"] == 2
+ assert cursor.execute.call_count >= 3
+ connection.commit.assert_called_once()
+
+ def test_update_provider_compliance_scores_skips_incomplete_scan(
+ self, tenants_fixture, scans_fixture
+ ):
+ tenant = tenants_fixture[0]
+ scan = scans_fixture[1]
+ tenant_id = str(tenant.id)
+ scan_id = str(scan.id)
+
+ result = update_provider_compliance_scores(tenant_id, scan_id)
+
+ assert result["status"] == "skipped"
+ assert result["reason"] == "scan not completed"
+
+ def test_update_provider_compliance_scores_skips_no_completed_at(
+ self, tenants_fixture, scans_fixture
+ ):
+ tenant = tenants_fixture[0]
+ scan = scans_fixture[0]
+ tenant_id = str(tenant.id)
+ scan_id = str(scan.id)
+
+ scan.state = StateChoices.COMPLETED
+ scan.completed_at = None
+ scan.save()
+
+ result = update_provider_compliance_scores(tenant_id, scan_id)
+
+ assert result["status"] == "skipped"
+ assert result["reason"] == "no completed_at"
+
+ @patch("tasks.jobs.scan.psycopg_connection")
+ def test_update_provider_compliance_scores_executes_sql_queries(
+ self,
+ mock_psycopg_connection,
+ tenants_fixture,
+ providers_fixture,
+ scans_fixture,
+ settings,
+ ):
+ settings.DATABASES.setdefault("admin", settings.DATABASES["default"])
+ tenant = tenants_fixture[0]
+ scan = scans_fixture[0]
+ tenant_id = str(tenant.id)
+ scan_id = str(scan.id)
+
+ scan.state = StateChoices.COMPLETED
+ scan.completed_at = datetime.now(timezone.utc)
+ scan.save()
+
+ connection = MagicMock()
+ cursor = MagicMock()
+ cursor_context = MagicMock()
+ cursor_context.__enter__.return_value = cursor
+ cursor_context.__exit__.return_value = False
+ connection.cursor.return_value = cursor_context
+ connection.__enter__.return_value = connection
+ connection.__exit__.return_value = False
+
+ context_manager = MagicMock()
+ context_manager.__enter__.return_value = connection
+ context_manager.__exit__.return_value = False
+ mock_psycopg_connection.return_value = context_manager
+
+ cursor.rowcount = 1
+ cursor.fetchall.side_effect = [[("aws_cis_2.0",)], []]
+
+ result = update_provider_compliance_scores(tenant_id, scan_id)
+
+ assert result["status"] == "completed"
+
+ calls = [str(c) for c in cursor.execute.call_args_list]
+ assert any("provider_compliance_scores" in c for c in calls)
+ assert any("tenant_compliance_summaries" in c for c in calls)
+ assert any("pg_advisory_xact_lock" in c for c in calls)
diff --git a/api/src/backend/tasks/tests/test_tasks.py b/api/src/backend/tasks/tests/test_tasks.py
index ceb7f608db..376315d90d 100644
--- a/api/src/backend/tasks/tests/test_tasks.py
+++ b/api/src/backend/tasks/tests/test_tasks.py
@@ -730,7 +730,9 @@ class TestGenerateOutputs:
class TestScanCompleteTasks:
@patch("tasks.tasks.aggregate_attack_surface_task.apply_async")
- @patch("tasks.tasks.create_compliance_requirements_task.apply_async")
+ @patch("tasks.tasks.chain")
+ @patch("tasks.tasks.create_compliance_requirements_task.si")
+ @patch("tasks.tasks.update_provider_compliance_scores_task.si")
@patch("tasks.tasks.perform_scan_summary_task.si")
@patch("tasks.tasks.generate_outputs_task.si")
@patch("tasks.tasks.generate_compliance_reports_task.si")
@@ -741,15 +743,22 @@ class TestScanCompleteTasks:
mock_compliance_reports_task,
mock_outputs_task,
mock_scan_summary_task,
+ mock_update_compliance_scores_task,
mock_compliance_requirements_task,
+ mock_chain,
mock_attack_surface_task,
):
"""Test that scan complete tasks are properly orchestrated with optimized reports."""
_perform_scan_complete_tasks("tenant-id", "scan-id", "provider-id")
- # Verify compliance requirements task is called
+ # Verify compliance requirements task is called via chain
mock_compliance_requirements_task.assert_called_once_with(
- kwargs={"tenant_id": "tenant-id", "scan_id": "scan-id"},
+ tenant_id="tenant-id", scan_id="scan-id"
+ )
+
+ # Verify update provider compliance scores task is called via chain
+ mock_update_compliance_scores_task.assert_called_once_with(
+ tenant_id="tenant-id", scan_id="scan-id"
)
# Verify attack surface task is called
diff --git a/dashboard/compliance/cis_1_12_kubernetes.py b/dashboard/compliance/cis_1_12_kubernetes.py
new file mode 100644
index 0000000000..94558f33ad
--- /dev/null
+++ b/dashboard/compliance/cis_1_12_kubernetes.py
@@ -0,0 +1,24 @@
+import warnings
+
+from dashboard.common_methods import get_section_containers_cis
+
+warnings.filterwarnings("ignore")
+
+
+def get_table(data):
+ aux = data[
+ [
+ "REQUIREMENTS_ID",
+ "REQUIREMENTS_DESCRIPTION",
+ "REQUIREMENTS_ATTRIBUTES_SECTION",
+ "CHECKID",
+ "STATUS",
+ "REGION",
+ "ACCOUNTID",
+ "RESOURCEID",
+ ]
+ ].copy()
+
+ return get_section_containers_cis(
+ aux, "REQUIREMENTS_ID", "REQUIREMENTS_ATTRIBUTES_SECTION"
+ )
diff --git a/dashboard/compliance/cis_5_0_azure.py b/dashboard/compliance/cis_5_0_azure.py
new file mode 100644
index 0000000000..9d33cc67a8
--- /dev/null
+++ b/dashboard/compliance/cis_5_0_azure.py
@@ -0,0 +1,25 @@
+import warnings
+
+from dashboard.common_methods import get_section_containers_cis
+
+warnings.filterwarnings("ignore")
+
+
+def get_table(data):
+
+ aux = data[
+ [
+ "REQUIREMENTS_ID",
+ "REQUIREMENTS_DESCRIPTION",
+ "REQUIREMENTS_ATTRIBUTES_SECTION",
+ "CHECKID",
+ "STATUS",
+ "REGION",
+ "ACCOUNTID",
+ "RESOURCEID",
+ ]
+ ].copy()
+
+ return get_section_containers_cis(
+ aux, "REQUIREMENTS_ID", "REQUIREMENTS_ATTRIBUTES_SECTION"
+ )
diff --git a/dashboard/compliance/cis_6_0_m365.py b/dashboard/compliance/cis_6_0_m365.py
new file mode 100644
index 0000000000..94558f33ad
--- /dev/null
+++ b/dashboard/compliance/cis_6_0_m365.py
@@ -0,0 +1,24 @@
+import warnings
+
+from dashboard.common_methods import get_section_containers_cis
+
+warnings.filterwarnings("ignore")
+
+
+def get_table(data):
+ aux = data[
+ [
+ "REQUIREMENTS_ID",
+ "REQUIREMENTS_DESCRIPTION",
+ "REQUIREMENTS_ATTRIBUTES_SECTION",
+ "CHECKID",
+ "STATUS",
+ "REGION",
+ "ACCOUNTID",
+ "RESOURCEID",
+ ]
+ ].copy()
+
+ return get_section_containers_cis(
+ aux, "REQUIREMENTS_ID", "REQUIREMENTS_ATTRIBUTES_SECTION"
+ )
diff --git a/docs/AGENTS.md b/docs/AGENTS.md
index 47e24d419d..2f9efd405d 100644
--- a/docs/AGENTS.md
+++ b/docs/AGENTS.md
@@ -479,6 +479,66 @@ Effective headers and section titles enhance document readability and structure,
---
+## Version Badge for Feature Documentation
+
+The Version Badge component indicates when a specific feature or functionality was introduced in Prowler. This component is located at `docs/snippets/version-badge.mdx` and should be used consistently across the documentation.
+
+### When to Use the Version Badge
+
+Use the Version Badge when documenting:
+
+* New features added in a specific version.
+* New CLI options or flags.
+* New API endpoints or SDK methods.
+* New compliance frameworks or security checks.
+* Breaking changes or deprecated features (with appropriate context).
+
+### How to Use the Version Badge
+
+1. **Import the Component**
+
+ At the top of the MDX file, import the snippet:
+
+ ```mdx
+ import { VersionBadge } from "/snippets/version-badge.mdx"
+ ```
+
+2. **Place the Badge**
+
+ Insert the badge immediately after the section header or feature title:
+
+ ```mdx
+ ## New Feature Name
+
+
+
+ Description of the feature...
+ ```
+
+3. **Version Format**
+
+ Use semantic versioning format (e.g., `4.5.0`, `5.0.0`). Do not include the "v" prefix.
+
+### Placement Guidelines
+
+* Place the Version Badge on its own line, directly below the header.
+* Leave a blank line after the badge before continuing with the content.
+* For subsections, place the badge only if the subsection introduces something new independently from the parent section.
+
+**Example:**
+
+```mdx
+## Tag-Based Scanning
+
+import { VersionBadge } from "/snippets/version-badge.mdx"
+
+
+
+Tag-Based Scanning allows filtering resources by AWS tags during security assessments...
+```
+
+---
+
## Avoid Assumptions Regarding Audience’s Expertise
### Understand Your Audience’s Expertise
diff --git a/docs/developer-guide/ai-skills.mdx b/docs/developer-guide/ai-skills.mdx
new file mode 100644
index 0000000000..d595c3ab11
--- /dev/null
+++ b/docs/developer-guide/ai-skills.mdx
@@ -0,0 +1,216 @@
+---
+title: 'AI Skills System'
+---
+
+This guide explains the AI Skills system that provides on-demand context and patterns to AI agents working with the Prowler codebase.
+
+
+**What are AI Skills?** Skills are structured instructions that help AI agents (Claude Code, Cursor, Copilot, etc.) understand Prowler's conventions, patterns, and best practices.
+
+
+## Architecture Overview
+
+```mermaid
+graph LR
+ subgraph FLOW["AI Skills Architecture"]
+ A["AI Agent"] -->|"1. matches trigger"| B["AGENTS.md"]
+ B -->|"2. loads"| C["Skill"]
+ C -->|"3. provides"| D["Patterns
Templates
Commands"]
+ C -->|"4. references"| E["Local Docs"]
+ D --> F["Correct Output"]
+ E --> F
+ end
+
+ style A fill:#1e3a5f,stroke:#4a9eff,color:#fff
+ style B fill:#5c4d1a,stroke:#ffd700,color:#fff
+ style C fill:#1a4d1a,stroke:#4caf50,color:#fff
+ style E fill:#4a1a4d,stroke:#ba68c8,color:#fff
+ style F fill:#1a4d2e,stroke:#66bb6a,color:#fff
+```
+
+## How It Works
+
+```mermaid
+sequenceDiagram
+ participant U as User
+ participant A as AI Agent
+ participant R as AGENTS.md
+ participant S as Skill
+ participant AS as assets/
+ participant RF as references/
+ participant D as Local Docs
+
+ U->>A: "Create an AWS security check"
+
+ Note over A: Analyze request context
+
+ A->>R: Find matching skill trigger
+ R-->>A: prowler-sdk-check matches
+
+ A->>S: Load SKILL.md
+ S-->>A: Patterns, rules, templates, commands
+
+ Note over A: Need code template?
+
+ A->>AS: Read assets/aws_check.py
+ AS-->>A: Check implementation template
+
+ Note over A: Need more details?
+
+ A->>RF: Read references/metadata-docs.md
+ RF-->>A: Points to local docs
+
+ A->>D: Read docs/developer-guide/checks.mdx
+ D-->>A: Full documentation
+
+ Note over A: Execute with full context
+
+ A->>U: Creates check with correct patterns
+```
+
+## Before vs After
+
+```mermaid
+graph TD
+ subgraph COMPARISON["BEFORE vs AFTER"]
+ direction LR
+
+ subgraph BEFORE["Without Skills"]
+ B1["AI guesses conventions"]
+ B2["Wrong structure"]
+ B3["Multiple iterations"]
+ B4["Web searches for docs"]
+ B5["Inconsistent patterns"]
+ end
+
+ subgraph AFTER["With Skills"]
+ A1["AI loads exact patterns"]
+ A2["Correct structure"]
+ A3["First-time right"]
+ A4["Local docs referenced"]
+ A5["Consistent patterns"]
+ end
+ end
+
+ style BEFORE fill:#5c1a1a,stroke:#ef5350,color:#fff
+ style AFTER fill:#1a4d1a,stroke:#66bb6a,color:#fff
+```
+
+## Complete Architecture
+
+```mermaid
+flowchart TB
+ subgraph ENTRY["ENTRY POINT"]
+ AGENTS["AGENTS.md
━━━━━━━━━━━━━━━━━
• Available skills registry
• Skill → Trigger mapping
• Component navigation"]
+ end
+
+ subgraph SKILLS["SKILLS LIBRARY"]
+ direction TB
+
+ subgraph GENERIC["Generic Skills"]
+ G1["typescript"]
+ G2["react-19"]
+ G3["nextjs-15"]
+ G4["tailwind-4"]
+ G5["pytest"]
+ G6["playwright"]
+ G7["django-drf"]
+ G8["zod-4"]
+ G9["zustand-5"]
+ G10["ai-sdk-5"]
+ end
+
+ subgraph PROWLER["Prowler Skills"]
+ P1["prowler"]
+ P2["prowler-sdk-check"]
+ P3["prowler-api"]
+ P4["prowler-ui"]
+ P5["prowler-mcp"]
+ P6["prowler-provider"]
+ P7["prowler-compliance"]
+ P8["prowler-docs"]
+ P9["prowler-pr"]
+ end
+
+ subgraph TESTING["Testing Skills"]
+ T1["prowler-test-sdk"]
+ T2["prowler-test-api"]
+ T3["prowler-test-ui"]
+ end
+
+ subgraph META["Meta Skills"]
+ M1["skill-creator"]
+ end
+ end
+
+ subgraph STRUCTURE["SKILL STRUCTURE"]
+ direction LR
+
+ SKILLMD["SKILL.md
━━━━━━━━━━━━━━
• Frontmatter
• Critical patterns
• Decision trees
• Code examples
• Commands
• Keywords"]
+
+ ASSETS["assets/
━━━━━━━━━━━━━━
• Code templates
• JSON schemas
• Config examples"]
+
+ REFS["references/
━━━━━━━━━━━━━━
• Local doc paths
• No web URLs
• Single source"]
+ end
+
+ subgraph DOCS["DOCUMENTATION"]
+ direction TB
+ DD["docs/developer-guide/"]
+ D1["checks.mdx"]
+ D2["unit-testing.mdx"]
+ D3["provider.mdx"]
+ D4["mcp-server.mdx"]
+ D5["..."]
+
+ DD --> D1
+ DD --> D2
+ DD --> D3
+ DD --> D4
+ DD --> D5
+ end
+
+ ENTRY --> SKILLS
+ SKILLS --> STRUCTURE
+ SKILLMD --> ASSETS
+ SKILLMD --> REFS
+ REFS -.->|"points to"| DOCS
+
+ style ENTRY fill:#1e3a5f,stroke:#4a9eff,color:#fff
+ style GENERIC fill:#5c4d1a,stroke:#ffd700,color:#fff
+ style PROWLER fill:#1a4d1a,stroke:#66bb6a,color:#fff
+ style TESTING fill:#4d1a3d,stroke:#f06292,color:#fff
+ style META fill:#4a1a4d,stroke:#ba68c8,color:#fff
+ style STRUCTURE fill:#5c3d1a,stroke:#ffb74d,color:#fff
+ style DOCS fill:#1a3d4d,stroke:#4dd0e1,color:#fff
+```
+
+## Skills Included
+
+| Type | Skills |
+|------|--------|
+| **Generic** | typescript, react-19, nextjs-15, tailwind-4, pytest, playwright, django-drf, zod-4, zustand-5, ai-sdk-5 |
+| **Prowler** | prowler, prowler-sdk-check, prowler-api, prowler-ui, prowler-mcp, prowler-provider, prowler-compliance, prowler-docs, prowler-pr |
+| **Testing** | prowler-test-sdk, prowler-test-api, prowler-test-ui |
+| **Meta** | skill-creator |
+
+## Skill Structure
+
+Each skill follows the [Agent Skills spec](https://agentskills.io):
+
+```
+skills/{skill-name}/
+├── SKILL.md # Patterns, rules, decision trees
+├── assets/ # Code templates, schemas
+└── references/ # Links to local docs (single source of truth)
+```
+
+## Key Design Decisions
+
+1. **Self-contained skills** - Critical patterns inline for fast loading
+2. **Local doc references** - No web URLs, points to `docs/developer-guide/*.mdx`
+3. **Single source of truth** - Skills reference docs, no duplication
+4. **On-demand loading** - AI loads only what's needed for the task
+
+## Creating New Skills
+
+Use the `skill-creator` meta-skill to create new skills that follow the Agent Skills spec. See `AGENTS.md` for the full list of available skills and their triggers.
diff --git a/docs/developer-guide/alibabacloud-details.mdx b/docs/developer-guide/alibabacloud-details.mdx
new file mode 100644
index 0000000000..4c21e17b29
--- /dev/null
+++ b/docs/developer-guide/alibabacloud-details.mdx
@@ -0,0 +1,212 @@
+---
+title: 'Alibaba Cloud Provider'
+---
+
+This page details the [Alibaba Cloud](https://www.alibabacloud.com/) provider implementation in Prowler.
+
+By default, Prowler will audit all the Alibaba Cloud regions that are available. To configure it, follow the [Alibaba Cloud getting started guide](/user-guide/providers/alibabacloud/getting-started-alibabacloud).
+
+## Alibaba Cloud Provider Classes Architecture
+
+The Alibaba Cloud provider implementation follows the general [Provider structure](/developer-guide/provider). This section focuses on the Alibaba Cloud-specific implementation, highlighting how the generic provider concepts are realized for Alibaba Cloud in Prowler. For a full overview of the provider pattern, base classes, and extension guidelines, see [Provider documentation](/developer-guide/provider).
+
+### Main Class
+
+- **Location:** [`prowler/providers/alibabacloud/alibabacloud_provider.py`](https://github.com/prowler-cloud/prowler/blob/master/prowler/providers/alibabacloud/alibabacloud_provider.py)
+- **Base Class:** Inherits from `Provider` (see [base class details](https://github.com/prowler-cloud/prowler/blob/master/prowler/providers/common/provider.py)).
+- **Purpose:** Central orchestrator for Alibaba Cloud-specific logic, session management, credential validation, and configuration.
+- **Key Alibaba Cloud Responsibilities:**
+ - Initializes and manages Alibaba Cloud sessions (supports Access Keys, STS Temporary Credentials, RAM Role Assumption, ECS RAM Role, OIDC Authentication, and Credentials URI).
+ - Validates credentials using STS GetCallerIdentity.
+ - Loads and manages configuration, mutelist, and fixer settings.
+ - Discovers and manages Alibaba Cloud regions.
+ - Provides properties and methods for downstream Alibaba Cloud service classes to access session, identity, and configuration data.
+
+### Data Models
+
+- **Location:** [`prowler/providers/alibabacloud/models.py`](https://github.com/prowler-cloud/prowler/blob/master/prowler/providers/alibabacloud/models.py)
+- **Purpose:** Define structured data for Alibaba Cloud identity, session, credentials, and region info.
+- **Key Alibaba Cloud Models:**
+ - `AlibabaCloudCallerIdentity`: Stores caller identity information from STS GetCallerIdentity (account_id, principal_id, arn, identity_type).
+ - `AlibabaCloudIdentityInfo`: Holds Alibaba Cloud identity metadata including account ID, user info, profile, and audited regions.
+ - `AlibabaCloudCredentials`: Stores credentials (access_key_id, access_key_secret, security_token).
+ - `AlibabaCloudRegion`: Represents an Alibaba Cloud region with region_id and region_name.
+ - `AlibabaCloudSession`: Manages the session and provides methods to create service clients.
+
+### `AlibabaCloudService` (Service Base Class)
+
+- **Location:** [`prowler/providers/alibabacloud/lib/service/service.py`](https://github.com/prowler-cloud/prowler/blob/master/prowler/providers/alibabacloud/lib/service/service.py)
+- **Purpose:** Abstract base class that all Alibaba Cloud service-specific classes inherit from. This implements the generic service pattern (described in [service page](/developer-guide/services#service-base-class)) specifically for Alibaba Cloud.
+- **Key Alibaba Cloud Responsibilities:**
+ - Receives an `AlibabacloudProvider` instance to access session, identity, and configuration.
+ - Manages regional clients for services that are region-specific.
+ - Provides `__threading_call__` method to make API calls in parallel by region or resource.
+ - Exposes common audit context (`audited_account`, `audited_account_name`, `audit_resources`, `audit_config`) to subclasses.
+
+### Exception Handling
+
+- **Location:** [`prowler/providers/alibabacloud/exceptions/exceptions.py`](https://github.com/prowler-cloud/prowler/blob/master/prowler/providers/alibabacloud/exceptions/exceptions.py)
+- **Purpose:** Custom exception classes for Alibaba Cloud-specific error handling.
+- **Key Alibaba Cloud Exceptions:**
+ - `AlibabaCloudClientError`: General client errors
+ - `AlibabaCloudNoCredentialsError`: No credentials found
+ - `AlibabaCloudInvalidCredentialsError`: Invalid credentials provided
+ - `AlibabaCloudSetUpSessionError`: Session setup failures
+ - `AlibabaCloudAssumeRoleError`: RAM role assumption failures
+ - `AlibabaCloudInvalidRegionError`: Invalid region specified
+ - `AlibabaCloudHTTPError`: HTTP/API errors
+
+### Session and Utility Helpers
+
+- **Location:** [`prowler/providers/alibabacloud/lib/`](https://github.com/prowler-cloud/prowler/tree/master/prowler/providers/alibabacloud/lib/)
+- **Purpose:** Helpers for argument parsing, mutelist management, and other cross-cutting concerns.
+
+## Specific Patterns in Alibaba Cloud Services
+
+The generic service pattern is described in [service page](/developer-guide/services#service-structure-and-initialisation). You can find all the currently implemented services in the following locations:
+
+- Directly in the code, in location [`prowler/providers/alibabacloud/services/`](https://github.com/prowler-cloud/prowler/tree/master/prowler/providers/alibabacloud/services)
+- In the [Prowler Hub](https://hub.prowler.com/) for a more human-readable view.
+
+The best reference to understand how to implement a new service is following the [service implementation documentation](/developer-guide/services#adding-a-new-service) and taking other services already implemented as reference. In next subsection you can find a list of common patterns that are used across all Alibaba Cloud services.
+
+### Alibaba Cloud Service Common Patterns
+
+- Services communicate with Alibaba Cloud using the official Alibaba Cloud Python SDKs. Documentation for individual services can be found in the [Alibaba Cloud SDK documentation](https://www.alibabacloud.com/help/en/sdk).
+- Every Alibaba Cloud service class inherits from `AlibabaCloudService`, ensuring access to session, identity, configuration, and client utilities.
+- The constructor (`__init__`) always calls `super().__init__` with the service name, provider, and optionally `global_service=True` for services that are not regional (e.g., RAM).
+- Resource containers **must** be initialized in the constructor. For regional services, resources are typically stored in dictionaries keyed by region and resource ID.
+- All Alibaba Cloud resources are represented as Pydantic `BaseModel` classes, providing type safety and structured access to resource attributes.
+- Alibaba Cloud SDK functions are wrapped in try/except blocks, with specific handling for errors, always logging errors.
+- Regional services use `self.regional_clients` to maintain clients for each audited region.
+- The `__threading_call__` method is used for parallel execution across regions or resources.
+
+### Example Service Implementation
+
+```python
+from prowler.lib.logger import logger
+from prowler.providers.alibabacloud.lib.service.service import AlibabaCloudService
+
+
+class MyService(AlibabaCloudService):
+ def __init__(self, provider):
+ # Initialize parent class with service name
+ super().__init__("myservice", provider)
+
+ # Initialize resource containers
+ self.resources = {}
+
+ # Discover resources using threading
+ self.__threading_call__(self._describe_resources)
+
+ def _describe_resources(self, regional_client):
+ try:
+ region = regional_client.region
+ response = regional_client.describe_resources()
+
+ for resource in response.body.resources:
+ self.resources[resource.id] = MyResource(
+ id=resource.id,
+ name=resource.name,
+ region=region,
+ # ... other attributes
+ )
+ except Exception as error:
+ logger.error(
+ f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
+ )
+```
+
+## Specific Patterns in Alibaba Cloud Checks
+
+The Alibaba Cloud checks pattern is described in [checks page](/developer-guide/checks). You can find all the currently implemented checks:
+
+- Directly in the code, within each service folder, each check has its own folder named after the name of the check. (e.g. [`prowler/providers/alibabacloud/services/ram/ram_no_root_access_key/`](https://github.com/prowler-cloud/prowler/tree/master/prowler/providers/alibabacloud/services/ram/ram_no_root_access_key))
+- In the [Prowler Hub](https://hub.prowler.com/) for a more human-readable view.
+
+The best reference to understand how to implement a new check is following the [check implementation documentation](/developer-guide/checks#creating-a-check) and taking other similar checks as reference.
+
+### Check Report Class
+
+The `CheckReportAlibabaCloud` class models a single finding for an Alibaba Cloud resource in a check report. It is defined in [`prowler/lib/check/models.py`](https://github.com/prowler-cloud/prowler/blob/master/prowler/lib/check/models.py) and inherits from the generic `Check_Report` base class.
+
+#### Purpose
+
+`CheckReportAlibabaCloud` extends the base report structure with Alibaba Cloud-specific fields, enabling detailed tracking of the resource, resource ID, ARN, and region associated with each finding.
+
+#### Constructor and Attribute Population
+
+When you instantiate `CheckReportAlibabaCloud`, you must provide the check metadata and a resource object. The class will attempt to automatically populate its Alibaba Cloud-specific attributes from the resource, using the following logic:
+
+- **`resource_id`**:
+ - Uses `resource.id` if present.
+ - Otherwise, uses `resource.name` if present.
+ - Defaults to an empty string if not available.
+
+- **`resource_arn`**:
+ - Uses `resource.arn` if present.
+ - Defaults to an empty string if not available.
+
+- **`region`**:
+ - Uses `resource.region` if present.
+ - Defaults to an empty string if not available.
+
+If the resource object does not contain the required attributes, you must set them manually in the check logic.
+
+Other attributes are inherited from the `Check_Report` class, from which you **always** have to set the `status` and `status_extended` attributes in the check logic.
+
+#### Example Usage
+
+```python
+from prowler.lib.check.models import Check, CheckReportAlibabaCloud
+from prowler.providers.alibabacloud.services.myservice.myservice_client import myservice_client
+
+
+class myservice_example_check(Check):
+ def execute(self) -> list[CheckReportAlibabaCloud]:
+ findings = []
+
+ for resource in myservice_client.resources.values():
+ report = CheckReportAlibabaCloud(
+ metadata=self.metadata(),
+ resource=resource
+ )
+ report.region = resource.region
+ report.resource_id = resource.id
+ report.resource_arn = f"acs:myservice::{myservice_client.audited_account}:resource/{resource.id}"
+
+ if resource.is_compliant:
+ report.status = "PASS"
+ report.status_extended = f"Resource {resource.name} is compliant."
+ else:
+ report.status = "FAIL"
+ report.status_extended = f"Resource {resource.name} is not compliant."
+
+ findings.append(report)
+
+ return findings
+```
+
+## Authentication Methods
+
+The Alibaba Cloud provider supports multiple authentication methods, prioritized in the following order:
+
+1. **Credentials URI** - Retrieve credentials from an external URI endpoint
+2. **OIDC Role Authentication** - For applications running in ACK with RRSA enabled
+3. **ECS RAM Role** - For ECS instances with attached RAM roles
+4. **RAM Role Assumption** - Cross-account access with role assumption
+5. **STS Temporary Credentials** - Pre-obtained temporary credentials
+6. **Permanent Access Keys** - Static access key credentials
+7. **Default Credential Chain** - Automatic credential discovery
+
+For detailed authentication configuration, see the [Authentication documentation](/user-guide/providers/alibabacloud/authentication).
+
+## Regions
+
+Alibaba Cloud has multiple regions across the globe. By default, Prowler audits all available regions. You can specify specific regions using the `--regions` CLI argument:
+
+```bash
+prowler alibabacloud --regions cn-hangzhou cn-shanghai
+```
+
+The list of supported regions is maintained in [`prowler/providers/alibabacloud/config.py`](https://github.com/prowler-cloud/prowler/blob/master/prowler/providers/alibabacloud/config.py).
diff --git a/docs/developer-guide/introduction.mdx b/docs/developer-guide/introduction.mdx
index 9d1ea742b1..3f77c53ab5 100644
--- a/docs/developer-guide/introduction.mdx
+++ b/docs/developer-guide/introduction.mdx
@@ -6,6 +6,10 @@ Thanks for your interest in contributing to Prowler!
Prowler can be extended in various ways. This guide provides the different ways to contribute and how to get started.
+
+Maintainers will assess whether a change fits the project roadmap and scope before merging.
+
+
## Contributing to Prowler
### Review Current Issues
@@ -32,6 +36,9 @@ Prowler is constantly evolving. Contributions to checks, services, or integratio
If you would like to extend Prowler to work with a new cloud provider, this typically involves setting up new services and checks to ensure compatibility.
+
+ Need to ensure Prowler supports a specific compliance framework? Add new security compliance frameworks to map checks against regulatory or industry standards.
+
Want to tailor how results are displayed or exported? You can add custom output formats.
@@ -213,4 +220,4 @@ pipx install "git+https://github.com/prowler-cloud/prowler.git@branch-name"
Replace `branch-name` with the name of the branch you want to test. This will install Prowler in an isolated environment, allowing you to try out the changes safely.
-For more details on testing go to the [Testing section](/developer-guide/unit-testing) of this documentation.
\ No newline at end of file
+For more details on testing go to the [Testing section](/developer-guide/unit-testing) of this documentation.
diff --git a/docs/developer-guide/lighthouse-architecture.mdx b/docs/developer-guide/lighthouse-architecture.mdx
new file mode 100644
index 0000000000..38ee50c5b8
--- /dev/null
+++ b/docs/developer-guide/lighthouse-architecture.mdx
@@ -0,0 +1,407 @@
+---
+title: 'Lighthouse AI Architecture'
+---
+
+This document describes the internal architecture of Prowler Lighthouse AI, enabling developers to understand how components interact and where to add new functionality.
+
+
+**Looking for user documentation?** See:
+- [Lighthouse AI Overview](/getting-started/products/prowler-lighthouse-ai) - Capabilities and FAQs
+- [How Lighthouse AI Works](/user-guide/tutorials/prowler-app-lighthouse) - Configuration and usage
+- [Multi-LLM Provider Setup](/user-guide/tutorials/prowler-app-lighthouse-multi-llm) - Provider configuration
+
+
+## Architecture Overview
+
+Lighthouse AI operates as a Langchain-based agent that connects Large Language Models (LLMs) with Prowler security data through the Model Context Protocol (MCP).
+
+
+
+
+### Three-Tier Architecture
+
+The system follows a three-tier architecture:
+
+1. **Frontend (Next.js)**: Chat interface, message rendering, model selection
+2. **API Route**: Request handling, authentication, stream transformation
+3. **Langchain Agent**: LLM orchestration, tool calling through MCP
+
+### Request Flow
+
+When a user sends a message through the Lighthouse chat interface, the system processes it through several stages:
+
+1. **User Submits a Message**.
+ The chat component (`ui/components/lighthouse/chat.tsx`) captures the user's question (e.g., "What are my critical findings in AWS?") and sends it as an HTTP POST request to the backend API route.
+
+2. **Authentication and Context Assembly**.
+ The API route (`ui/app/api/lighthouse/analyst/route.ts`) validates the user's session, extracts the JWT token (stored via `auth-context.ts`), and gathers context including the tenant's business context and current security posture data (assembled in `data.ts`).
+
+3. **Agent Initialization**.
+ The workflow orchestrator (`ui/lib/lighthouse/workflow.ts`) creates a Langchain agent configured with:
+ - The selected LLM, instantiated through the factory (`llm-factory.ts`)
+ - A system prompt containing available tools and instructions (`system-prompt.ts`)
+ - Two meta-tools (`describe_tool` and `execute_tool`) for accessing Prowler data
+
+4. **LLM Reasoning and Tool Calling**.
+ The agent sends the conversation to the LLM, which decides whether to respond directly or call tools to fetch data. When tools are needed, the meta-tools in `ui/lib/lighthouse/tools/meta-tool.ts` interact with the MCP client (`mcp-client.ts`) to:
+ - First call `describe_tool` to understand the tool's parameters
+ - Then call `execute_tool` to retrieve data from the MCP Server
+ - Continue reasoning with the returned data
+
+5. **Streaming Response**.
+ As the LLM generates its response, the stream handler (`ui/lib/lighthouse/analyst-stream.ts`) transforms Langchain events into UI-compatible messages and streams tokens back to the browser in real-time using Server-Sent Events. The stream includes both text tokens and tool execution events (displayed as "chain of thought").
+
+6. **Message Rendering**.
+ The frontend receives the stream and renders it through `message-item.tsx` with markdown formatting. Any tool calls that occurred during reasoning are displayed via `chain-of-thought-display.tsx`.
+
+## Frontend Components
+
+Frontend components reside in `ui/components/lighthouse/` and handle the chat interface and configuration workflows.
+
+### Core Components
+
+| Component | Location | Purpose |
+|-----------|----------|---------|
+| `chat.tsx` | `ui/components/lighthouse/` | Main chat interface managing message history and input handling |
+| `message-item.tsx` | `ui/components/lighthouse/` | Individual message rendering with markdown support |
+| `select-model.tsx` | `ui/components/lighthouse/` | Model and provider selection dropdown |
+| `chain-of-thought-display.tsx` | `ui/components/lighthouse/` | Displays tool calls and reasoning steps during execution |
+
+### Configuration Components
+
+| Component | Location | Purpose |
+|-----------|----------|---------|
+| `lighthouse-settings.tsx` | `ui/components/lighthouse/` | Settings panel for business context and preferences |
+| `connect-llm-provider.tsx` | `ui/components/lighthouse/` | Provider connection workflow |
+| `llm-providers-table.tsx` | `ui/components/lighthouse/` | Provider management table |
+| `forms/delete-llm-provider-form.tsx` | `ui/components/lighthouse/forms/` | Provider deletion confirmation dialog |
+
+### Supporting Components
+
+| Component | Location | Purpose |
+|-----------|----------|---------|
+| `banner.tsx` / `banner-client.tsx` | `ui/components/lighthouse/` | Status banners and notifications |
+| `workflow/` | `ui/components/lighthouse/workflow/` | Multi-step configuration workflows |
+| `ai-elements/` | `ui/components/lighthouse/ai-elements/` | Custom UI primitives for chat interface (input, select, dropdown, tooltip) |
+
+## Library Code
+
+Core library code resides in `ui/lib/lighthouse/` and handles agent orchestration, MCP communication, and stream processing.
+
+### Workflow Orchestrator
+
+**Location:** `ui/lib/lighthouse/workflow.ts`
+
+The workflow module serves as the core orchestrator, responsible for:
+
+- Initializing the Langchain agent with system prompt and tools
+- Loading tenant configuration (default provider, model, business context)
+- Creating the LLM instance through the factory
+- Generating dynamic tool listings from available MCP tools
+
+```typescript
+// Simplified workflow initialization
+export async function initLighthouseWorkflow(runtimeConfig?: RuntimeConfig) {
+ await initializeMCPClient();
+
+ const toolListing = generateToolListing();
+ const systemPrompt = LIGHTHOUSE_SYSTEM_PROMPT_TEMPLATE.replace(
+ "{{TOOL_LISTING}}",
+ toolListing,
+ );
+
+ const llm = createLLM({
+ provider: providerType,
+ model: modelId,
+ credentials,
+ // ...
+ });
+
+ return createAgent({
+ model: llm,
+ tools: [describeTool, executeTool],
+ systemPrompt,
+ });
+}
+```
+
+### MCP Client Manager
+
+**Location:** `ui/lib/lighthouse/mcp-client.ts`
+
+The MCP client manages connections to the Prowler MCP Server using a singleton pattern:
+
+- **Connection Management**: Retry logic with configurable attempts and delays
+- **Tool Discovery**: Fetches available tools from MCP server on initialization
+- **Authentication Injection**: Automatically adds JWT tokens to `prowler_app_*` tool calls
+- **Reconnection**: Supports forced reconnection after server restarts
+
+Key constants:
+- `MAX_RETRY_ATTEMPTS`: 3 connection attempts
+- `RETRY_DELAY_MS`: 2000ms between retries
+- `RECONNECT_INTERVAL_MS`: 5 minutes before retry after failure
+
+```typescript
+// Authentication injection for Prowler App tools
+private handleBeforeToolCall = ({ name, args }) => {
+ // Only inject auth for prowler_app_* tools (user-specific data)
+ if (!name.startsWith("prowler_app_")) {
+ return { args };
+ }
+
+ const accessToken = getAuthContext();
+ return {
+ args,
+ headers: { Authorization: `Bearer ${accessToken}` },
+ };
+};
+```
+
+### Meta-Tools
+
+**Location:** `ui/lib/lighthouse/tools/meta-tool.ts`
+
+Instead of registering all MCP tools directly with the agent, Lighthouse uses two meta-tools for dynamic tool discovery and execution:
+
+| Tool | Purpose |
+|------|---------|
+| `describe_tool` | Retrieves full schema and parameter details for a specific tool |
+| `execute_tool` | Executes a tool with provided parameters |
+
+This pattern reduces the number of tools the LLM must track while maintaining access to all MCP capabilities.
+
+### Additional Library Modules
+
+| Module | Location | Purpose |
+|--------|----------|---------|
+| `analyst-stream.ts` | `ui/lib/lighthouse/` | Transforms Langchain stream events to UI message format |
+| `llm-factory.ts` | `ui/lib/lighthouse/` | Creates LLM instances for OpenAI, Bedrock, and OpenAI-compatible providers |
+| `system-prompt.ts` | `ui/lib/lighthouse/` | System prompt template with dynamic tool listing injection |
+| `auth-context.ts` | `ui/lib/lighthouse/` | AsyncLocalStorage for JWT token propagation across async boundaries |
+| `types.ts` | `ui/lib/lighthouse/` | TypeScript type definitions |
+| `constants.ts` | `ui/lib/lighthouse/` | Configuration constants and error messages |
+| `utils.ts` | `ui/lib/lighthouse/` | Message conversion and model parameter extraction |
+| `validation.ts` | `ui/lib/lighthouse/` | Input validation utilities |
+| `data.ts` | `ui/lib/lighthouse/` | Current data section generation for context enrichment |
+
+## API Route
+
+**Location:** `ui/app/api/lighthouse/analyst/route.ts`
+
+The API route handles chat requests and manages the streaming response pipeline:
+
+1. **Request Parsing**: Extracts messages, model, and provider from request body
+2. **Authentication**: Validates session and extracts access token
+3. **Context Assembly**: Gathers business context and current data
+4. **Agent Initialization**: Creates Langchain agent with runtime configuration
+5. **Stream Processing**: Transforms agent events to UI-compatible format
+6. **Error Handling**: Captures errors with Sentry integration
+
+```typescript
+export async function POST(req: Request) {
+ const { messages, model, provider } = await req.json();
+
+ const session = await auth();
+ if (!session?.accessToken) {
+ return Response.json({ error: "Unauthorized" }, { status: 401 });
+ }
+
+ return await authContextStorage.run(accessToken, async () => {
+ const app = await initLighthouseWorkflow(runtimeConfig);
+ const agentStream = app.streamEvents({ messages }, { version: "v2" });
+
+ // Transform stream events to UI format
+ const stream = new ReadableStream({
+ async start(controller) {
+ for await (const streamEvent of agentStream) {
+ // Handle on_chat_model_stream, on_tool_start, on_tool_end, etc.
+ }
+ },
+ });
+
+ return createUIMessageStreamResponse({ stream });
+ });
+}
+```
+
+## Backend Components
+
+Backend components handle LLM provider configuration, model management, and credential storage.
+
+### Database Models
+
+**Location:** `api/src/backend/api/models.py`
+
+| Model | Purpose |
+|-------|---------|
+| `LighthouseProviderConfiguration` | Per-tenant LLM provider credentials (encrypted with Fernet) |
+| `LighthouseTenantConfiguration` | Tenant-level settings including business context and default provider/model |
+| `LighthouseProviderModels` | Available models per provider configuration |
+
+All models implement Row-Level Security (RLS) for tenant isolation.
+
+#### LighthouseProviderConfiguration
+
+Stores provider-specific credentials for each tenant:
+
+- **provider_type**: `openai`, `bedrock`, or `openai_compatible`
+- **credentials**: Encrypted JSON containing API keys or AWS credentials
+- **base_url**: Custom endpoint for OpenAI-compatible providers
+- **is_active**: Connection validation status
+
+#### LighthouseTenantConfiguration
+
+Stores tenant-wide Lighthouse settings:
+
+- **business_context**: Optional context for personalized responses
+- **default_provider**: Default LLM provider type
+- **default_models**: JSON mapping provider types to default model IDs
+
+#### LighthouseProviderModels
+
+Catalogs available models for each provider:
+
+- **model_id**: Provider-specific model identifier
+- **model_name**: Human-readable display name
+- **default_parameters**: Optional model-specific parameters
+
+### Background Jobs
+
+**Location:** `api/src/backend/tasks/jobs/lighthouse_providers.py`
+
+#### check_lighthouse_provider_connection
+
+Validates provider credentials by making a test API call:
+
+- OpenAI: Lists models via `client.models.list()`
+- Bedrock: Lists foundation models via `bedrock_client.list_foundation_models()`
+- OpenAI-compatible: Lists models via custom base URL
+
+Updates `is_active` status based on connection result.
+
+#### refresh_lighthouse_provider_models
+
+Synchronizes available models from provider APIs:
+
+- Fetches current model catalog from provider
+- Filters out non-chat models (DALL-E, Whisper, TTS, embeddings)
+- Upserts model records in `LighthouseProviderModels`
+- Removes stale models no longer available
+
+**Excluded OpenAI model prefixes:**
+```python
+EXCLUDED_OPENAI_MODEL_PREFIXES = (
+ "dall-e", "whisper", "tts-", "sora",
+ "text-embedding", "text-moderation",
+ # Legacy models
+ "text-davinci", "davinci", "curie", "babbage", "ada",
+)
+```
+
+## MCP Server Integration
+
+Lighthouse AI communicates with the Prowler MCP Server to access security data. For detailed MCP Server architecture, see [Extending the MCP Server](/developer-guide/mcp-server).
+
+### Tool Namespacing
+
+MCP tools are organized into three namespaces based on authentication requirements:
+
+| Namespace | Auth Required | Description |
+|-----------|---------------|-------------|
+| `prowler_app_*` | Yes (JWT) | Prowler Cloud/App tools for findings, providers, scans, resources |
+| `prowler_hub_*` | No | Security checks catalog, compliance frameworks |
+| `prowler_docs_*` | No | Documentation search and retrieval |
+
+### Authentication Flow
+
+1. User authenticates with Prowler App, receiving a JWT token
+2. Token is stored in session and propagated via `authContextStorage`
+3. MCP client injects `Authorization: Bearer ` header for `prowler_app_*` calls
+4. MCP Server validates token and applies RLS filtering
+
+### Tool Execution Pattern
+
+The agent uses meta-tools rather than direct tool registration:
+
+```
+Agent needs data → describe_tool("prowler_app_search_findings")
+ → Returns parameter schema → execute_tool with parameters
+ → MCP client adds auth header → MCP Server executes
+ → Results returned to agent → Agent continues reasoning
+```
+
+## Extension Points
+
+### Adding New LLM Providers
+
+To add a new LLM provider:
+
+1. **Frontend**: Update `ui/lib/lighthouse/llm-factory.ts` with provider-specific initialization
+2. **Backend**: Add provider type to `LighthouseProviderConfiguration.LLMProviderChoices`
+3. **Jobs**: Add credential extraction and model fetching in `lighthouse_providers.py`
+4. **UI**: Add connection workflow in `ui/components/lighthouse/workflow/`
+
+### Modifying System Prompt
+
+The system prompt template lives in `ui/lib/lighthouse/system-prompt.ts`. The `{{TOOL_LISTING}}` placeholder is dynamically replaced with available MCP tools during agent initialization.
+
+### Adding Stream Events
+
+To handle new Langchain stream events, modify `ui/lib/lighthouse/analyst-stream.ts`. Current handlers include:
+
+- `on_chat_model_stream`: Token-by-token text streaming
+- `on_chat_model_end`: Model completion with tool call detection
+- `on_tool_start`: Tool execution started
+- `on_tool_end`: Tool execution completed
+
+### Adding MCP Tools
+
+See [Extending the MCP Server](/developer-guide/mcp-server) for detailed instructions on adding new tools to the Prowler MCP Server.
+
+## Configuration
+
+### Environment Variables
+
+| Variable | Description |
+|----------|-------------|
+| `PROWLER_MCP_SERVER_URL` | MCP server endpoint (e.g., `https://mcp.prowler.com/mcp`) |
+
+### Database Configuration
+
+Provider credentials are stored encrypted in `LighthouseProviderConfiguration`:
+
+- **OpenAI**: `{"api_key": "sk-..."}`
+- **Bedrock**: `{"access_key_id": "...", "secret_access_key": "...", "region": "us-east-1"}` or `{"api_key": "...", "region": "us-east-1"}`
+- **OpenAI-compatible**: `{"api_key": "..."}` with `base_url` field
+
+### Tenant Configuration
+
+Business context and default settings are stored in `LighthouseTenantConfiguration`:
+
+```python
+{
+ "business_context": "Optional organization context for personalized responses",
+ "default_provider": "openai",
+ "default_models": {
+ "openai": "gpt-4o",
+ "bedrock": "anthropic.claude-3-5-sonnet-20240620-v1:0"
+ }
+}
+```
+
+## Related Documentation
+
+
+
+ Adding new tools to the Prowler MCP Server
+
+
+ Capabilities, FAQs, and limitations
+
+
+ Configuring multiple LLM providers
+
+
+ User-facing architecture and setup guide
+
+
diff --git a/docs/developer-guide/lighthouse.mdx b/docs/developer-guide/lighthouse.mdx
deleted file mode 100644
index 25afd51728..0000000000
--- a/docs/developer-guide/lighthouse.mdx
+++ /dev/null
@@ -1,140 +0,0 @@
----
-title: 'Extending Prowler Lighthouse AI'
----
-
-This guide helps developers customize and extend Prowler Lighthouse AI by adding or modifying AI agents.
-
-## Understanding AI Agents
-
-AI agents combine Large Language Models (LLMs) with specialized tools that provide environmental context. These tools can include API calls, system command execution, or any function-wrapped capability.
-
-### Types of AI Agents
-
-AI agents fall into two main categories:
-
-- **Autonomous Agents**: Freely chooses from available tools to complete tasks, adapting their approach based on context. They decide which tools to use and when.
-- **Workflow Agents**: Follows structured paths with predefined logic. They execute specific tool sequences and can include conditional logic.
-
-Prowler Lighthouse AI is an autonomous agent - selecting the right tool(s) based on the users query.
-
-
-To learn more about AI agents, read [Anthropic's blog post on building effective agents](https://www.anthropic.com/engineering/building-effective-agents).
-
-
-### LLM Dependency
-
-The autonomous nature of agents depends on the underlying LLM. Autonomous agents using identical system prompts and tools but powered by different LLM providers might approach user queries differently. Agent with one LLM might solve a problem efficiently, while with another it might take a different route or fail entirely.
-
-After evaluating multiple LLM providers (OpenAI, Gemini, Claude, LLama) based on tool calling features and response accuracy, we recommend using the `gpt-4o` model.
-
-## Prowler Lighthouse AI Architecture
-
-Prowler Lighthouse AI uses a multi-agent architecture orchestrated by the [Langgraph-Supervisor](https://www.npmjs.com/package/@langchain/langgraph-supervisor) library.
-
-### Architecture Components
-
-
-
-Prowler Lighthouse AI integrates with the NextJS application:
-
-- The [Langgraph-Supervisor](https://www.npmjs.com/package/@langchain/langgraph-supervisor) library integrates directly with NextJS
-- The system uses the authenticated user session to interact with the Prowler API server
-- Agents only access data the current user is authorized to view
-- Session management operates automatically, ensuring Role-Based Access Control (RBAC) is maintained
-
-## Available Prowler AI Agents
-
-The following specialized AI agents are available in Prowler:
-
-### Agent Overview
-
-- **provider_agent**: Fetches information about cloud providers connected to Prowler
-- **user_info_agent**: Retrieves information about Prowler users
-- **scans_agent**: Fetches information about Prowler scans
-- **compliance_agent**: Retrieves compliance overviews across scans
-- **findings_agent**: Fetches information about individual findings across scans
-- **overview_agent**: Retrieves overview information (providers, findings by status and severity, etc.)
-
-## How to Add New Capabilities
-
-### Updating the Supervisor Prompt
-
-The supervisor agent controls system behavior, tone, and capabilities. You can find the supervisor prompt at: [https://github.com/prowler-cloud/prowler/blob/master/ui/lib/lighthouse/prompts.ts](https://github.com/prowler-cloud/prowler/blob/master/ui/lib/lighthouse/prompts.ts)
-
-#### Supervisor Prompt Modifications
-
-Modifying the supervisor prompt allows you to:
-
-- Change personality or response style
-- Add new high-level capabilities
-- Modify task delegation to specialized agents
-- Set up guardrails (query types to answer or decline)
-
-
-The supervisor agent should not have its own tools. This design keeps the system modular and maintainable.
-
-
-### How to Create New Specialized Agents
-
-The supervisor agent and all specialized agents are defined in the `route.ts` file. The supervisor agent uses [langgraph-supervisor](https://www.npmjs.com/package/@langchain/langgraph-supervisor), while other agents use the prebuilt [create-react-agent](https://langchain-ai.github.io/langgraphjs/how-tos/create-react-agent/).
-
-To add new capabilities or all Lighthouse AI to interact with other APIs, create additional specialized agents:
-
-1. First determine what the new agent would do. Create a detailed prompt defining the agent's purpose and capabilities. You can see an example from [here](https://github.com/prowler-cloud/prowler/blob/master/ui/lib/lighthouse/prompts.ts#L359-L385).
-
-Ensure that the new agent's capabilities don't collide with existing agents. For example, if there's already a *findings_agent* that talks to findings APIs don't create a new agent to do the same.
-
-
-2. Create necessary tools for the agents to access specific data or perform actions. A tool is a specialized function that extends the capabilities of LLM by allowing it to access external data or APIs. A tool is triggered by LLM based on the description of the tool and the user's query.
-For example, the description of `getScanTool` is "Fetches detailed information about a specific scan by its ID." If the description doesn't convey what the tool is capable of doing, LLM will not invoke the function. If the description of `getScanTool` was set to something random or not set at all, LLM will not answer queries like "Give me the critical issues from the scan ID xxxxxxxxxxxxxxx"
-
-Ensure that one tool is added to one agent only. Adding tools is optional. There can be agents with no tools at all.
-
-
-3. Use the `createReactAgent` function to define a new agent. For example, the rolesAgent name is "roles_agent" and has access to call tools "*getRolesTool*" and "*getRoleTool*"
-```js
-const rolesAgent = createReactAgent({
- llm: llm,
- tools: [getRolesTool, getRoleTool],
- name: "roles_agent",
- prompt: rolesAgentPrompt,
-});
-```
-
-4. Create a detailed prompt defining the agent's purpose and capabilities.
-
-5. Add the new agent to the available agents list:
-```js
-const agents = [
- userInfoAgent,
- providerAgent,
- overviewAgent,
- scansAgent,
- complianceAgent,
- findingsAgent,
- rolesAgent, // New agent added here
-];
-// Create supervisor workflow
-const workflow = createSupervisor({
- agents: agents,
- llm: supervisorllm,
- prompt: supervisorPrompt,
- outputMode: "last_message",
-});
-```
-
-6. Update the supervisor's system prompt to summarize the new agent's capabilities.
-
-### Best Practices for Agent Development
-
-When developing new agents or capabilities:
-
-- **Clear Responsibility Boundaries**: Each agent should have a defined purpose with minimal overlap. No two agents should access the same tools or different tools accessing the same Prowler APIs.
-- **Minimal Data Access**: Agents should only request the data they need, keeping requests specific to minimize context window usage, cost, and response time.
-- **Thorough Prompting:** Ensure agent prompts include clear instructions about:
- - The agent's purpose and limitations
- - How to use its tools
- - How to format responses for the supervisor
- - Error handling procedures (Optional)
-- **Security Considerations:** Agents should never modify data or access sensitive information like secrets or credentials.
-- **Testing:** Thoroughly test new agents with various queries before deploying to production.
diff --git a/docs/docs.json b/docs/docs.json
index c25a4f363a..f8c20a258a 100644
--- a/docs/docs.json
+++ b/docs/docs.json
@@ -284,8 +284,9 @@
"developer-guide/outputs",
"developer-guide/integrations",
"developer-guide/security-compliance-framework",
- "developer-guide/lighthouse",
- "developer-guide/mcp-server"
+ "developer-guide/lighthouse-architecture",
+ "developer-guide/mcp-server",
+ "developer-guide/ai-skills"
]
},
{
@@ -294,6 +295,7 @@
"developer-guide/aws-details",
"developer-guide/azure-details",
"developer-guide/gcp-details",
+ "developer-guide/alibabacloud-details",
"developer-guide/kubernetes-details",
"developer-guide/m365-details",
"developer-guide/github-details",
diff --git a/docs/getting-started/products/prowler-lighthouse-ai.mdx b/docs/getting-started/products/prowler-lighthouse-ai.mdx
index 629699e41a..6f197ae546 100644
--- a/docs/getting-started/products/prowler-lighthouse-ai.mdx
+++ b/docs/getting-started/products/prowler-lighthouse-ai.mdx
@@ -59,6 +59,14 @@ Prowler Lighthouse AI is powerful, but there are limitations:
- **NextJS session dependence**: If your Prowler application session expires or logs out, Lighthouse AI will error out. Refresh and log back in to continue.
- **Response quality**: The response quality depends on the selected LLM provider and model. Choose models with strong tool-calling capabilities for best results. We recommend `gpt-5` model from OpenAI.
+## Extending Lighthouse AI
+
+Lighthouse AI retrieves data through Prowler MCP. To add new capabilities, extend the Prowler MCP Server with additional tools and Lighthouse AI discovers them automatically.
+
+For development details, see:
+- [Lighthouse AI Architecture](/developer-guide/lighthouse-architecture) - Internal architecture and extension points
+- [Extending the MCP Server](/developer-guide/mcp-server) - Adding new tools to Prowler MCP
+
### Getting Help
If you encounter issues with Prowler Lighthouse AI or have suggestions for improvements, please [reach out through our Slack channel](https://goto.prowler.com/slack).
@@ -67,94 +75,6 @@ If you encounter issues with Prowler Lighthouse AI or have suggestions for impro
The following API endpoints are accessible to Prowler Lighthouse AI. Data from the following API endpoints could be shared with LLM provider depending on the scope of user's query:
-#### Accessible API Endpoints
-
-**User Management:**
-
-- List all users - `/api/v1/users`
-- Retrieve the current user's information - `/api/v1/users/me`
-
-**Provider Management:**
-
-- List all providers - `/api/v1/providers`
-- Retrieve data from a provider - `/api/v1/providers/{id}`
-
-**Scan Management:**
-
-- List all scans - `/api/v1/scans`
-- Retrieve data from a specific scan - `/api/v1/scans/{id}`
-
-**Resource Management:**
-
-- List all resources - `/api/v1/resources`
-- Retrieve data for a resource - `/api/v1/resources/{id}`
-
-**Findings Management:**
-
-- List all findings - `/api/v1/findings`
-- Retrieve data from a specific finding - `/api/v1/findings/{id}`
-- Retrieve metadata values from findings - `/api/v1/findings/metadata`
-
-**Overview Data:**
-
-- Get aggregated findings data - `/api/v1/overviews/findings`
-- Get findings data by severity - `/api/v1/overviews/findings_severity`
-- Get aggregated provider data - `/api/v1/overviews/providers`
-- Get findings data by service - `/api/v1/overviews/services`
-
-**Compliance Management:**
-
-- List compliance overviews (optionally filter by scan) - `/api/v1/compliance-overviews`
-- Retrieve data from a specific compliance overview - `/api/v1/compliance-overviews/{id}`
-
-#### Excluded API Endpoints
-
-Not all Prowler API endpoints are integrated with Lighthouse AI. They are intentionally excluded for the following reasons:
-
-- OpenAI/other LLM providers shouldn't have access to sensitive data (like fetching provider secrets and other sensitive config)
-- Users queries don't need responses from those API endpoints (ex: tasks, tenant details, downloading zip file, etc.)
-
-**Excluded Endpoints:**
-
-**User Management:**
-
-- List specific users information - `/api/v1/users/{id}`
-- List user memberships - `/api/v1/users/{user_pk}/memberships`
-- Retrieve membership data from the user - `/api/v1/users/{user_pk}/memberships/{id}`
-
-**Tenant Management:**
-
-- List all tenants - `/api/v1/tenants`
-- Retrieve data from a tenant - `/api/v1/tenants/{id}`
-- List tenant memberships - `/api/v1/tenants/{tenant_pk}/memberships`
-- List all invitations - `/api/v1/tenants/invitations`
-- Retrieve data from tenant invitation - `/api/v1/tenants/invitations/{id}`
-
-**Security and Configuration:**
-
-- List all secrets - `/api/v1/providers/secrets`
-- Retrieve data from a secret - `/api/v1/providers/secrets/{id}`
-- List all provider groups - `/api/v1/provider-groups`
-- Retrieve data from a provider group - `/api/v1/provider-groups/{id}`
-
-**Reports and Tasks:**
-
-- Download zip report - `/api/v1/scans/{v1}/report`
-- List all tasks - `/api/v1/tasks`
-- Retrieve data from a specific task - `/api/v1/tasks/{id}`
-
-**Lighthouse AI Configuration:**
-
-- List LLM providers - `/api/v1/lighthouse/providers`
-- Retrieve LLM provider - `/api/v1/lighthouse/providers/{id}`
-- List available models - `/api/v1/lighthouse/models`
-- Retrieve tenant configuration - `/api/v1/lighthouse/configuration`
-
-
-Agents only have access to hit GET endpoints. They don't have access to other HTTP methods.
-
-
-
## FAQs
**1. Which LLM providers are supported?**
@@ -167,13 +87,21 @@ Lighthouse AI supports three providers:
For detailed configuration instructions, see [Using Multiple LLM Providers with Lighthouse](/user-guide/tutorials/prowler-app-lighthouse-multi-llm).
-**2. Why a multi-agent supervisor model?**
+**2. Why some models don't appear in Lighthouse AI?**
-Context windows are limited. While demo data fits inside the context window, querying real-world data often exceeds it. A multi-agent architecture is used so different agents fetch different sizes of data and respond with the minimum required data to the supervisor. This spreads the context window usage across agents.
+LLM providers offer different types of models. Not every model can be integrated with Lighthouse AI (for example, text-to-speech, vision, embedding, computer use, etc.).
+
+Lighthouse AI requires models that support:
+
+- Text input
+- Text output
+- Tool calling
+
+Lighthouse AI [automatically filters](https://github.com/prowler-cloud/prowler/blob/master/api/src/backend/tasks/jobs/lighthouse_providers.py#L341-L353) out models that do not support these capabilities, so some provider models may not appear in the Lighthouse AI model list.
**3. Is my security data shared with LLM providers?**
-Minimal data is shared to generate useful responses. Agents can access security findings and remediation details when needed. Provider secrets are protected by design and cannot be read. The LLM provider credentials configured with Lighthouse AI are only accessible to our NextJS server and are never sent to the LLM providers. Resource metadata (names, tags, account/project IDs, etc) may be shared with the configured LLM provider based on query requirements.
+Minimal data is shared to generate useful responses. Agent can access security findings and remediation details when needed. Provider secrets are protected by design and cannot be read. The LLM provider credentials configured with Lighthouse AI are only accessible to the Next.js server and are never sent to the LLM providers. Resource metadata (names, tags, account/project IDs, etc.) may be shared with the configured LLM provider based on query requirements.
**4. Can the Lighthouse AI change my cloud environment?**
diff --git a/docs/images/cli/add-cloud-provider.png b/docs/images/cli/add-cloud-provider.png
index dd42e73047..d8f19b2054 100644
Binary files a/docs/images/cli/add-cloud-provider.png and b/docs/images/cli/add-cloud-provider.png differ
diff --git a/docs/images/cli/cloud-providers-page.png b/docs/images/cli/cloud-providers-page.png
index 0581e0132f..dcbce73a10 100644
Binary files a/docs/images/cli/cloud-providers-page.png and b/docs/images/cli/cloud-providers-page.png differ
diff --git a/docs/images/cli/lighthouse-architecture.png b/docs/images/cli/lighthouse-architecture.png
deleted file mode 100644
index 63202ce7c7..0000000000
Binary files a/docs/images/cli/lighthouse-architecture.png and /dev/null differ
diff --git a/docs/images/lighthouse-architecture-dark.png b/docs/images/lighthouse-architecture-dark.png
new file mode 100644
index 0000000000..0ed77712c5
Binary files /dev/null and b/docs/images/lighthouse-architecture-dark.png differ
diff --git a/docs/images/lighthouse-architecture-light.png b/docs/images/lighthouse-architecture-light.png
new file mode 100644
index 0000000000..076240ccb2
Binary files /dev/null and b/docs/images/lighthouse-architecture-light.png differ
diff --git a/docs/images/providers/add-alibaba-account-id.png b/docs/images/providers/add-alibaba-account-id.png
new file mode 100644
index 0000000000..2e49f995cc
Binary files /dev/null and b/docs/images/providers/add-alibaba-account-id.png differ
diff --git a/docs/images/providers/alibaba-account-id.png b/docs/images/providers/alibaba-account-id.png
new file mode 100644
index 0000000000..89e79b1f20
Binary files /dev/null and b/docs/images/providers/alibaba-account-id.png differ
diff --git a/docs/images/providers/alibaba-connect-via-credentials-static.png b/docs/images/providers/alibaba-connect-via-credentials-static.png
new file mode 100644
index 0000000000..f9b9a00ee6
Binary files /dev/null and b/docs/images/providers/alibaba-connect-via-credentials-static.png differ
diff --git a/docs/images/providers/alibaba-connect-via-credentials.png b/docs/images/providers/alibaba-connect-via-credentials.png
new file mode 100644
index 0000000000..f9b9a00ee6
Binary files /dev/null and b/docs/images/providers/alibaba-connect-via-credentials.png differ
diff --git a/docs/images/providers/alibaba-credentials-form.png b/docs/images/providers/alibaba-credentials-form.png
new file mode 100644
index 0000000000..3cefc0253c
Binary files /dev/null and b/docs/images/providers/alibaba-credentials-form.png differ
diff --git a/docs/images/providers/alibaba-get-role-arn.png b/docs/images/providers/alibaba-get-role-arn.png
new file mode 100644
index 0000000000..d95822a18e
Binary files /dev/null and b/docs/images/providers/alibaba-get-role-arn.png differ
diff --git a/docs/images/providers/alibaba-ram-role-overview.png b/docs/images/providers/alibaba-ram-role-overview.png
new file mode 100644
index 0000000000..a9420d0749
Binary files /dev/null and b/docs/images/providers/alibaba-ram-role-overview.png differ
diff --git a/docs/images/providers/launch-scan-alibaba.png b/docs/images/providers/launch-scan-alibaba.png
new file mode 100644
index 0000000000..324e1334c4
Binary files /dev/null and b/docs/images/providers/launch-scan-alibaba.png differ
diff --git a/docs/images/providers/select-alibaba-cloud.png b/docs/images/providers/select-alibaba-cloud.png
new file mode 100644
index 0000000000..8b65931472
Binary files /dev/null and b/docs/images/providers/select-alibaba-cloud.png differ
diff --git a/docs/images/providers/select-auth-method-alibaba.png b/docs/images/providers/select-auth-method-alibaba.png
new file mode 100644
index 0000000000..ffd0083bf0
Binary files /dev/null and b/docs/images/providers/select-auth-method-alibaba.png differ
diff --git a/docs/images/prowler-app/add-cloud-provider.png b/docs/images/prowler-app/add-cloud-provider.png
index dd42e73047..d8f19b2054 100644
Binary files a/docs/images/prowler-app/add-cloud-provider.png and b/docs/images/prowler-app/add-cloud-provider.png differ
diff --git a/docs/images/prowler-app/cloud-providers-page.png b/docs/images/prowler-app/cloud-providers-page.png
index 0581e0132f..dcbce73a10 100644
Binary files a/docs/images/prowler-app/cloud-providers-page.png and b/docs/images/prowler-app/cloud-providers-page.png differ
diff --git a/docs/images/prowler-app/lighthouse-architecture.png b/docs/images/prowler-app/lighthouse-architecture.png
deleted file mode 100644
index 63202ce7c7..0000000000
Binary files a/docs/images/prowler-app/lighthouse-architecture.png and /dev/null differ
diff --git a/docs/introduction.mdx b/docs/introduction.mdx
index 44438f7278..928a527a88 100644
--- a/docs/introduction.mdx
+++ b/docs/introduction.mdx
@@ -23,19 +23,19 @@
The supported providers right now are:
-| Provider | Support | Interface |
-| -------------------------------------------------------------------------------- | ---------- | ------------ |
-| [AWS](/user-guide/providers/aws/getting-started-aws) | Official | UI, API, CLI |
-| [Azure](/user-guide/providers/azure/getting-started-azure) | Official | UI, API, CLI |
-| [Google Cloud](/user-guide/providers/gcp/getting-started-gcp) | Official | UI, API, CLI |
-| [Kubernetes](/user-guide/providers/kubernetes/getting-started-k8s) | Official | UI, API, CLI |
-| [M365](/user-guide/providers/microsoft365/getting-started-m365) | Official | UI, API, CLI |
-| [Github](/user-guide/providers/github/getting-started-github) | Official | UI, API, CLI |
-| [Oracle Cloud](/user-guide/providers/oci/getting-started-oci) | Official | UI, API, CLI |
-| [Infra as Code](/user-guide/providers/iac/getting-started-iac) | Official | UI, API, CLI |
-| [MongoDB Atlas](/user-guide/providers/mongodbatlas/getting-started-mongodbatlas) | Official | UI, API, CLI |
-| [LLM](/user-guide/providers/llm/getting-started-llm) | Official | CLI |
-| **NHN** | Unofficial | CLI |
+| Provider | Support | Audit Scope/Entities | Interface |
+| -------------------------------------------------------------------------------- | ---------- | ---------------------------- | ------------ |
+| [AWS](/user-guide/providers/aws/getting-started-aws) | Official | Accounts | UI, API, CLI |
+| [Azure](/user-guide/providers/azure/getting-started-azure) | Official | Subscriptions | UI, API, CLI |
+| [Google Cloud](/user-guide/providers/gcp/getting-started-gcp) | Official | Projects | UI, API, CLI |
+| [Kubernetes](/user-guide/providers/kubernetes/getting-started-k8s) | Official | Clusters | UI, API, CLI |
+| [M365](/user-guide/providers/microsoft365/getting-started-m365) | Official | Tenants | UI, API, CLI |
+| [Github](/user-guide/providers/github/getting-started-github) | Official | Organizations / Repositories | UI, API, CLI |
+| [Oracle Cloud](/user-guide/providers/oci/getting-started-oci) | Official | Tenancies / Compartments | UI, API, CLI |
+| [Infra as Code](/user-guide/providers/iac/getting-started-iac) | Official | Repositories | UI, API, CLI |
+| [MongoDB Atlas](/user-guide/providers/mongodbatlas/getting-started-mongodbatlas) | Official | Organizations | UI, API, CLI |
+| [LLM](/user-guide/providers/llm/getting-started-llm) | Official | Models | CLI |
+| **NHN** | Unofficial | Tenants | CLI |
For more information about the checks and compliance of each provider visit [Prowler Hub](https://hub.prowler.com).
diff --git a/docs/user-guide/cli/img/add-cloud-provider.png b/docs/user-guide/cli/img/add-cloud-provider.png
index dd42e73047..d8f19b2054 100644
Binary files a/docs/user-guide/cli/img/add-cloud-provider.png and b/docs/user-guide/cli/img/add-cloud-provider.png differ
diff --git a/docs/user-guide/cli/img/cloud-providers-page.png b/docs/user-guide/cli/img/cloud-providers-page.png
index 0581e0132f..dcbce73a10 100644
Binary files a/docs/user-guide/cli/img/cloud-providers-page.png and b/docs/user-guide/cli/img/cloud-providers-page.png differ
diff --git a/docs/user-guide/cli/img/lighthouse-architecture.png b/docs/user-guide/cli/img/lighthouse-architecture.png
deleted file mode 100644
index 63202ce7c7..0000000000
Binary files a/docs/user-guide/cli/img/lighthouse-architecture.png and /dev/null differ
diff --git a/docs/user-guide/cli/tutorials/configuration_file.mdx b/docs/user-guide/cli/tutorials/configuration_file.mdx
index fe8cbf4f39..73f9ff3999 100644
--- a/docs/user-guide/cli/tutorials/configuration_file.mdx
+++ b/docs/user-guide/cli/tutorials/configuration_file.mdx
@@ -97,6 +97,7 @@ The following list includes all the GCP checks with configurable variables that
| Check Name | Value | Type |
|---------------------------------------------------------------|--------------------------------------------------|-----------------|
+| `compute_configuration_changes` | `compute_audit_log_lookback_days` | Integer |
| `compute_instance_group_multiple_zones` | `mig_min_zones` | Integer |
## Kubernetes
@@ -553,6 +554,9 @@ gcp:
# GCP Compute Configuration
# gcp.compute_public_address_shodan
shodan_api_key: null
+ # gcp.compute_configuration_changes
+ # Number of days to look back for Compute Engine configuration changes in audit logs
+ compute_audit_log_lookback_days: 1
# gcp.compute_instance_group_multiple_zones
# Minimum number of zones a MIG should span for high availability
mig_min_zones: 2
diff --git a/docs/user-guide/img/add-cloud-provider.png b/docs/user-guide/img/add-cloud-provider.png
index dd42e73047..d8f19b2054 100644
Binary files a/docs/user-guide/img/add-cloud-provider.png and b/docs/user-guide/img/add-cloud-provider.png differ
diff --git a/docs/user-guide/img/cloud-providers-page.png b/docs/user-guide/img/cloud-providers-page.png
index 0581e0132f..dcbce73a10 100644
Binary files a/docs/user-guide/img/cloud-providers-page.png and b/docs/user-guide/img/cloud-providers-page.png differ
diff --git a/docs/user-guide/img/lighthouse-architecture.png b/docs/user-guide/img/lighthouse-architecture.png
deleted file mode 100644
index 63202ce7c7..0000000000
Binary files a/docs/user-guide/img/lighthouse-architecture.png and /dev/null differ
diff --git a/docs/user-guide/providers/alibabacloud/getting-started-alibabacloud.mdx b/docs/user-guide/providers/alibabacloud/getting-started-alibabacloud.mdx
index 469f026b5a..c38cc42e37 100644
--- a/docs/user-guide/providers/alibabacloud/getting-started-alibabacloud.mdx
+++ b/docs/user-guide/providers/alibabacloud/getting-started-alibabacloud.mdx
@@ -2,26 +2,111 @@
title: 'Getting Started With Alibaba Cloud on Prowler'
---
-## Prowler CLI
+import { VersionBadge } from "/snippets/version-badge.mdx"
-### Configure Alibaba Cloud Credentials
+Prowler supports Alibaba Cloud both from the CLI and from Prowler Cloud. This guide walks you through the requirements, how to connect the provider in the UI, and how to run scans from the command line.
-Prowler requires Alibaba Cloud credentials to perform security checks. Authentication is available through the following methods (in order of priority):
+## Prerequisites
-1. **Credentials URI** (Recommended for centralized credential services)
-2. **OIDC Role Authentication** (Recommended for ACK/Kubernetes)
-3. **ECS RAM Role** (Recommended for ECS instances)
-4. **RAM Role Assumption** (Recommended for cross-account access)
-5. **STS Temporary Credentials**
-6. **Permanent Access Keys**
-7. **Default Credential Chain**
+Before you begin, make sure you have:
+
+1. An **Alibaba Cloud Account ID** (visible in the Alibaba Cloud Console under your profile).
+2. **Credentials** with appropriate permissions:
+ - **RAM User with Access Keys**: For static credential authentication.
+ - **RAM Role**: For cross-account access using role assumption (recommended).
+3. The required permissions for Prowler to audit your resources. See the [Alibaba Cloud Authentication](/user-guide/providers/alibabacloud/authentication) guide for the full list of required permissions.
+
+
+
+ Onboard Alibaba Cloud using Prowler Cloud
+
+
+ Onboard Alibaba Cloud using Prowler CLI
+
+
+
+## Prowler Cloud
+
+
+
+### Step 1: Get Your Alibaba Cloud Account ID
+
+1. Log in to the [Alibaba Cloud Console](https://home.console.alibabacloud.com/)
+2. Click on your profile avatar in the top-right corner
+3. Locate and copy your Account ID
+
+
+
+### Step 2: Access Prowler Cloud or Prowler App
+
+1. Navigate to [Prowler Cloud](https://cloud.prowler.com/) or launch [Prowler App](/user-guide/tutorials/prowler-app)
+2. Go to "Configuration" > "Cloud Providers"
+
+ 
+
+3. Click "Add Cloud Provider"
+
+ 
+
+4. Select "Alibaba Cloud"
+
+ 
+
+5. Enter your Alibaba Cloud Account ID and optionally provide a friendly alias
+
+ 
+
+### Step 3: Choose and Provide Authentication
+
+After the Account ID is in place, select the authentication method that matches your Alibaba Cloud setup:
+
+
+
+#### RAM Role Assumption (Recommended)
+
+Use this method for secure cross-account access. For detailed instructions on how to create the RAM role, see the [Authentication guide](/user-guide/providers/alibabacloud/authentication#ram-role-assumption-recommended-for-cross-account).
+
+1. Enter the **Role ARN** (format: `acs:ram:::role/`)
+2. Enter the **Access Key ID** and **Access Key Secret** of the RAM user that will assume the role
+
+ 
+
+
+The RAM user whose credentials you provide must have permission to assume the target role. For more details, see the [Alibaba Cloud AssumeRole API documentation](https://www.alibabacloud.com/help/en/ram/developer-reference/api-sts-2015-04-01-assumerole).
+
+
+#### Credentials (Static Access Keys)
+
+Use static credentials for quick scans (not recommended for production). For detailed setup, see the [Authentication guide](/user-guide/providers/alibabacloud/authentication#permanent-access-keys).
+
+1. Enter the **Access Key ID** and **Access Key Secret**
+
+ 
-Prowler does not accept credentials through command-line arguments. Provide credentials through environment variables or the Alibaba Cloud credential chain.
-
+Static access keys are long-lived credentials. For production environments, consider using RAM Role Assumption instead.
-#### Option 1: Environment Variables (Permanent Credentials)
+### Step 4: Launch the Scan
+
+1. Click "Next" to review your configuration
+2. Click "Launch Scan" to start auditing your Alibaba Cloud account
+
+ 
+
+---
+
+## Prowler CLI
+
+
+
+You can also run Alibaba Cloud assessments directly from the CLI. Both command-line flags and environment variables are supported.
+
+### Step 1: Select an Authentication Method
+
+Choose one of the following authentication methods. For the complete list and detailed configuration, see the [Authentication guide](/user-guide/providers/alibabacloud/authentication).
+
+#### Environment Variables
```bash
export ALIBABA_CLOUD_ACCESS_KEY_ID="your-access-key-id"
@@ -29,104 +114,49 @@ export ALIBABA_CLOUD_ACCESS_KEY_SECRET="your-access-key-secret"
prowler alibabacloud
```
-#### Option 2: Environment Variables (STS Temporary Credentials)
-
-```bash
-export ALIBABA_CLOUD_ACCESS_KEY_ID="your-sts-access-key-id"
-export ALIBABA_CLOUD_ACCESS_KEY_SECRET="your-sts-access-key-secret"
-export ALIBABA_CLOUD_SECURITY_TOKEN="your-sts-security-token"
-prowler alibabacloud
-```
-
-#### Option 3: RAM Role Assumption (Environment Variables)
+#### RAM Role Assumption
```bash
export ALIBABA_CLOUD_ACCESS_KEY_ID="your-access-key-id"
export ALIBABA_CLOUD_ACCESS_KEY_SECRET="your-access-key-secret"
export ALIBABA_CLOUD_ROLE_ARN="acs:ram::123456789012:role/ProwlerAuditRole"
-export ALIBABA_CLOUD_ROLE_SESSION_NAME="ProwlerAssessmentSession" # Optional
prowler alibabacloud
```
-#### Option 4: RAM Role Assumption (CLI + Environment Variables)
+#### ECS RAM Role (for ECS instances)
```bash
-# Set credentials via environment variables
-export ALIBABA_CLOUD_ACCESS_KEY_ID="your-access-key-id"
-export ALIBABA_CLOUD_ACCESS_KEY_SECRET="your-access-key-secret"
-# Specify role via CLI argument
-prowler alibabacloud --role-arn acs:ram::123456789012:role/ProwlerAuditRole --role-session-name ProwlerAssessmentSession
-```
-
-#### Option 5: ECS Instance Metadata (ECS RAM Role)
-
-```bash
-# When running on an ECS instance with an attached RAM role
prowler alibabacloud --ecs-ram-role RoleName
-
-# Or using environment variable
-export ALIBABA_CLOUD_ECS_METADATA="RoleName"
-prowler alibabacloud
```
-#### Option 6: OIDC Role Authentication (for ACK/Kubernetes)
+### Step 2: Run the First Scan
-```bash
-# For applications running in ACK (Alibaba Container Service for Kubernetes) with RRSA enabled
-export ALIBABA_CLOUD_ROLE_ARN="acs:ram::123456789012:role/YourRole"
-export ALIBABA_CLOUD_OIDC_PROVIDER_ARN="acs:ram::123456789012:oidc-provider/ack-rrsa-provider"
-export ALIBABA_CLOUD_OIDC_TOKEN_FILE="/var/run/secrets/tokens/oidc-token"
-export ALIBABA_CLOUD_ROLE_SESSION_NAME="ProwlerOIDCSession" # Optional
-prowler alibabacloud
-
-# Or using CLI argument
-prowler alibabacloud --oidc-role-arn acs:ram::123456789012:role/YourRole
-```
-
-#### Option 7: Credentials URI (External Credential Service)
-
-```bash
-# Retrieve credentials from an external URI endpoint
-export ALIBABA_CLOUD_CREDENTIALS_URI="http://localhost:8080/credentials"
-prowler alibabacloud
-
-# Or using CLI argument
-prowler alibabacloud --credentials-uri http://localhost:8080/credentials
-```
-
-#### Option 8: Default Credential Chain
-
-The SDK automatically checks credentials in the following order:
-1. Environment variables (`ALIBABA_CLOUD_*` or `ALIYUN_*`)
-2. OIDC authentication (if OIDC environment variables are set)
-3. Configuration file (`~/.aliyun/config.json`)
-4. ECS instance metadata (if running on ECS)
-5. Credentials URI (if `ALIBABA_CLOUD_CREDENTIALS_URI` is set)
+#### Scan all regions
```bash
prowler alibabacloud
```
-### Specify Regions
-
-To run checks only in specific regions:
+#### Scan specific regions
```bash
prowler alibabacloud --regions cn-hangzhou cn-shanghai
```
-### Run Specific Checks
-
-To run specific checks:
+#### Run specific checks
```bash
prowler alibabacloud --checks ram_no_root_access_key ram_user_mfa_enabled_console_access
```
-### Run Compliance Framework
-
-To run a specific compliance framework:
+#### Run a compliance framework
```bash
prowler alibabacloud --compliance cis_2.0_alibabacloud
```
+
+### Additional Tips
+
+- Combine flags (for example, `--checks` or `--services`) just like with other providers.
+- Use `--output-modes` to export findings in JSON, CSV, ASFF, etc.
+- For more authentication options (OIDC, Credentials URI, STS), see the [Authentication guide](/user-guide/providers/alibabacloud/authentication).
diff --git a/docs/user-guide/providers/aws/role-assumption.mdx b/docs/user-guide/providers/aws/role-assumption.mdx
index ef45e56441..b714beafbd 100644
--- a/docs/user-guide/providers/aws/role-assumption.mdx
+++ b/docs/user-guide/providers/aws/role-assumption.mdx
@@ -69,18 +69,18 @@ If your IAM Role is configured with Multi-Factor Authentication (MFA), use `--mf
## Creating a Role for One or Multiple Accounts
-To create an IAM role that can be assumed in one or multiple AWS accounts, use either a CloudFormation Stack or StackSet with the provided [template](https://github.com/prowler-cloud/prowler/blob/master/permissions/create_role_to_assume_cfn.yaml).
+To create an IAM role that can be assumed in one or multiple AWS accounts, use either a CloudFormation Stack or StackSet with the provided [template](https://github.com/prowler-cloud/prowler/blob/master/permissions/templates/cloudformation/prowler-scan-role.yml).
The template requires the following parameters:
-- **AuthorisedARN:** The ARN of the user or role authorized to assume the created role (e.g., `arn:aws:iam::123456789012:root`)
- **ExternalId:** A unique identifier to prevent the [confused deputy problem](https://docs.aws.amazon.com/IAM/latest/UserGuide/confused-deputy.html)
-- **ProwlerRoleName:** *(Optional)* Name of the IAM role (default: `ProwlerScanRole`)
+- **AccountId:** *(Optional)* AWS Account ID that will assume the role (default: Prowler Cloud account)
+- **IAMPrincipal:** *(Optional)* The IAM principal allowed to assume the role (default: `role/prowler*`)
-When running Prowler, include the External ID using the `-I/--external-id` flag:
+When running Prowler CLI, include the External ID using the `-I/--external-id` flag:
```sh
-prowler aws -R arn:aws:iam:::role/ -I
+prowler aws -R arn:aws:iam:::role/ProwlerScan -I
```
diff --git a/docs/user-guide/providers/azure/authentication.mdx b/docs/user-guide/providers/azure/authentication.mdx
index 4e33da0775..57746e2d60 100644
--- a/docs/user-guide/providers/azure/authentication.mdx
+++ b/docs/user-guide/providers/azure/authentication.mdx
@@ -246,12 +246,223 @@ prowler azure --az-cli-auth
*Available only for Prowler CLI*
-Authenticate via Azure Managed Identity (when running on Azure resources):
+Authenticate via Azure Managed Identity when running Prowler on Azure resources (VMs, Container Instances, Azure Functions, etc.):
```console
prowler azure --managed-identity-auth
```
+### Prerequisites
+
+Before using Managed Identity authentication, the following steps are required:
+
+1. **Enable Managed Identity** on the Azure resource (e.g., VM, Container Instance)
+2. **Assign the required permissions** to the Managed Identity on the target subscription(s) to scan
+
+
+A common misconception is that enabling a Managed Identity on a resource automatically grants it permissions. **This is not the case.** Without explicit role assignments, Prowler will be unable to scan subscriptions and will return authorization errors, resulting in incomplete security assessments. The Managed Identity itself is a service principal that must be explicitly granted Reader and ProwlerRole permissions on each subscription to scan.
+
+
+### Step-by-Step Setup Guide
+
+#### Step 1: Enable Managed Identity on the Azure Resource
+
+
+
+ **Via Azure Portal:**
+ 1. Navigate to the VM in Azure Portal
+ 2. Select "Identity" from the left menu under "Security"
+ 3. Under "System assigned" tab, set Status to "On"
+ 4. Click "Save"
+ 5. Note the "Object (principal) ID" - this value is required for permission assignment
+
+ **Via Azure CLI:**
+ ```console
+ # Enable system-assigned managed identity
+ az vm identity assign --name --resource-group
+
+ # Get the principal ID
+ az vm identity show --name --resource-group --query principalId -o tsv
+ ```
+
+
+ **Via Azure CLI:**
+ ```console
+ # Enable system-assigned managed identity
+ az container create \
+ --resource-group \
+ --name \
+ --image \
+ --assign-identity
+
+ # Get the principal ID
+ az container show --resource-group --name --query identity.principalId -o tsv
+ ```
+
+
+
+#### Step 2: Assign Reader Role to the Managed Identity
+
+The Managed Identity needs the **Reader** role on each subscription to scan. This role must be assigned to the **Managed Identity's principal ID**, not the VM or resource itself.
+
+
+
+ 1. Navigate to the **target subscription** to scan (not the VM's resource group)
+ 2. Select "Access control (IAM)" from the left menu
+ 3. Click "+ Add" > "Add role assignment"
+ 4. Select "Reader" role, click "Next"
+ 5. Click "+ Select members"
+ 6. Search for the VM name or paste the Managed Identity's Object/Principal ID
+ 7. Select it and click "Select"
+ 8. Click "Review + assign"
+
+
+ When scanning a subscription different from where the VM is located, ensure the role is assigned on the **target subscription**, not the VM's subscription.
+
+
+
+ ```console
+ # Get the principal ID of the resource's managed identity
+ PRINCIPAL_ID=$(az vm identity show --name --resource-group --query principalId -o tsv)
+
+ # Assign Reader role on the target subscription
+ az role assignment create \
+ --role "Reader" \
+ --assignee-object-id $PRINCIPAL_ID \
+ --assignee-principal-type ServicePrincipal \
+ --scope /subscriptions/
+ ```
+
+
+
+#### Step 3: Create and Assign ProwlerRole to the Managed Identity
+
+The ProwlerRole is a custom role required for specific security checks. First, create the role if it does not exist, then assign it to the Managed Identity.
+
+
+
+ **Create the ProwlerRole:**
+ ```console
+ az role definition create --role-definition '{
+ "Name": "ProwlerRole",
+ "IsCustom": true,
+ "Description": "Role used for checks that require read-only access to Azure resources and are not covered by the Reader role.",
+ "AssignableScopes": ["/subscriptions/"],
+ "Actions": [
+ "Microsoft.Web/sites/host/listkeys/action",
+ "Microsoft.Web/sites/config/list/Action"
+ ]
+ }'
+ ```
+
+ **Assign ProwlerRole to the Managed Identity:**
+ ```console
+ # Get the principal ID if not already available
+ PRINCIPAL_ID=$(az vm identity show --name --resource-group --query principalId -o tsv)
+
+ # Assign ProwlerRole on the target subscription
+ az role assignment create \
+ --role "ProwlerRole" \
+ --assignee-object-id $PRINCIPAL_ID \
+ --assignee-principal-type ServicePrincipal \
+ --scope /subscriptions/
+ ```
+
+
+ Follow the same process as creating the ProwlerRole in the [Assigning ProwlerRole Permissions](/user-guide/providers/azure/authentication#assigning-prowlerrole-permissions-at-the-subscription-level) section, then assign it to the Managed Identity using the same steps as the Reader role assignment.
+
+
+
+#### Step 4: (Optional) Assign Microsoft Graph Permissions
+
+For Entra ID (Azure AD) checks, the Managed Identity needs Microsoft Graph API permissions: `Directory.Read.All`, `Policy.Read.All`, and optionally `UserAuthenticationMethod.Read.All`.
+
+
+Assigning Microsoft Graph API permissions to a Managed Identity requires Azure CLI or PowerShell - it cannot be done through the Azure Portal's standard role assignment interface.
+
+
+```console
+# Get the Managed Identity's principal ID
+PRINCIPAL_ID=$(az vm identity show --name --resource-group --query principalId -o tsv)
+
+# Get Microsoft Graph's service principal ID
+GRAPH_SP_ID=$(az ad sp list --display-name "Microsoft Graph" --query [0].id -o tsv)
+
+# Assign Directory.Read.All permission (App Role ID: 7ab1d382-f21e-4acd-a863-ba3e13f7da61)
+az rest --method POST \
+ --uri "https://graph.microsoft.com/v1.0/servicePrincipals/$PRINCIPAL_ID/appRoleAssignments" \
+ --headers "Content-Type=application/json" \
+ --body "{\"principalId\": \"$PRINCIPAL_ID\", \"resourceId\": \"$GRAPH_SP_ID\", \"appRoleId\": \"7ab1d382-f21e-4acd-a863-ba3e13f7da61\"}"
+
+# Assign Policy.Read.All permission (App Role ID: 246dd0d5-5bd0-4def-940b-0421030a5b68)
+az rest --method POST \
+ --uri "https://graph.microsoft.com/v1.0/servicePrincipals/$PRINCIPAL_ID/appRoleAssignments" \
+ --headers "Content-Type=application/json" \
+ --body "{\"principalId\": \"$PRINCIPAL_ID\", \"resourceId\": \"$GRAPH_SP_ID\", \"appRoleId\": \"246dd0d5-5bd0-4def-940b-0421030a5b68\"}"
+```
+
+#### Step 5: Run Prowler
+
+SSH or connect to the Azure resource and run Prowler:
+
+```console
+# Scan all accessible subscriptions
+prowler azure --managed-identity-auth
+
+# Scan specific subscription(s)
+prowler azure --managed-identity-auth --subscription-ids
+```
+
+
+Wait a few minutes after assigning roles for Azure to propagate permissions. Role assignments are not always immediately effective.
+
+
+### Troubleshooting
+
+#### Error: "No subscriptions were found, please check your permission assignments"
+
+**Cause:** The Managed Identity does not have the Reader role assigned on any subscription.
+
+**Solution:**
+- Verify the Managed Identity has the Reader role assigned on at least one subscription.
+- Wait a few minutes after role assignment for Azure to propagate permissions.
+- Verify role assignments:
+ ```console
+ az role assignment list --assignee --all
+ ```
+
+#### Error: "does not have authorization to perform action 'Microsoft.Resources/subscriptions/read'"
+
+**Cause:** The Managed Identity lacks the Reader role on the target subscription.
+
+**Solution:**
+- Ensure the Reader role is assigned to the **Managed Identity's principal ID**, not the VM resource.
+- Verify the role is assigned on the **target subscription** to scan, not just the VM's resource group.
+- Check role assignments:
+ ```console
+ az role assignment list --assignee --scope /subscriptions/
+ ```
+
+#### Error: "CredentialUnavailableError: ManagedIdentityCredential authentication unavailable"
+
+**Cause:** Managed Identity is not enabled on the resource, or Prowler is running outside of Azure.
+
+**Solution:**
+- Verify Managed Identity is enabled on the Azure resource.
+- Ensure Prowler is running from within the Azure resource (not a local machine).
+- Check Managed Identity status:
+ ```console
+ az vm identity show --name --resource-group
+ ```
+
+#### Error: Access token validation failure for Entra ID checks
+
+**Cause:** The Managed Identity lacks Microsoft Graph API permissions.
+
+**Solution:**
+- Assign the required Graph API permissions as shown in Step 4.
+- These permissions are optional for basic resource scanning but required for Entra ID security checks.
+
## Browser Authentication
*Available only for Prowler CLI*
diff --git a/docs/user-guide/providers/oci/authentication.mdx b/docs/user-guide/providers/oci/authentication.mdx
index 2ae73fb3af..7f0c493e76 100644
--- a/docs/user-guide/providers/oci/authentication.mdx
+++ b/docs/user-guide/providers/oci/authentication.mdx
@@ -164,7 +164,7 @@ prowler oci --profile PRODUCTION
Use a config file from a custom location:
```bash
-prowler oci --config-file /path/to/custom/config
+prowler oci --oci-config-file /path/to/custom/config
```
### Setting Up API Keys
@@ -377,7 +377,7 @@ ls -la ~/.oci/config
mkdir -p ~/.oci
# Specify custom location
-prowler oci --config-file /path/to/config
+prowler oci --oci-config-file /path/to/config
```
#### Error: "InvalidKeyOrSignature"
diff --git a/docs/user-guide/providers/oci/getting-started-oci.mdx b/docs/user-guide/providers/oci/getting-started-oci.mdx
index 80c008480d..8affa2f992 100644
--- a/docs/user-guide/providers/oci/getting-started-oci.mdx
+++ b/docs/user-guide/providers/oci/getting-started-oci.mdx
@@ -122,7 +122,7 @@ prowler oci --profile production
##### Using a Custom Config File
```bash
-prowler oci --config-file /path/to/custom/config
+prowler oci --oci-config-file /path/to/custom/config
```
#### Instance Principal Authentication
diff --git a/docs/user-guide/tutorials/prowler-app-lighthouse-multi-llm.mdx b/docs/user-guide/tutorials/prowler-app-lighthouse-multi-llm.mdx
index dbee857fa7..0a0179ee79 100644
--- a/docs/user-guide/tutorials/prowler-app-lighthouse-multi-llm.mdx
+++ b/docs/user-guide/tutorials/prowler-app-lighthouse-multi-llm.mdx
@@ -22,7 +22,7 @@ For Lighthouse AI to work properly, models **must** support all of the following
- **Text input**: Ability to receive text prompts.
- **Text output**: Ability to generate text responses.
-- **Tool calling**: Ability to invoke tools and functions.
+- **Tool calling**: Ability to invoke tools and functions to retrieve data from Prowler.
If any of these capabilities are missing, the model will not be compatible with Lighthouse AI.
diff --git a/docs/user-guide/tutorials/prowler-app-lighthouse.mdx b/docs/user-guide/tutorials/prowler-app-lighthouse.mdx
index 4dfb3fef8f..cf4886d2ee 100644
--- a/docs/user-guide/tutorials/prowler-app-lighthouse.mdx
+++ b/docs/user-guide/tutorials/prowler-app-lighthouse.mdx
@@ -8,24 +8,33 @@ import { VersionBadge } from "/snippets/version-badge.mdx"
Prowler Lighthouse AI integrates Large Language Models (LLMs) with Prowler security findings data.
-Here's what's happening behind the scenes:
+Behind the scenes, Lighthouse AI works as follows:
+
+- Lighthouse AI runs as a [Langchain agent](https://docs.langchain.com/oss/javascript/langchain/agents) in NextJS
+- The agent connects to the configured LLM provider to understand the prompt and decide what data is needed
+- The agent accesses Prowler data through [Prowler MCP](https://docs.prowler.com/getting-started/products/prowler-mcp), which exposes tools from multiple sources, including:
+ - Prowler Hub
+ - Prowler Docs
+ - Prowler App
+- Instead of calling every tool directly, the agent uses two meta-tools:
+ - `describe_tool` to retrieve a tool schema and parameter requirements.
+ - `execute_tool` to run the selected tool with the required input.
+- Based on the user's query and the data necessary to answer it, Lighthouse agent will invoke necessary Prowler MCP tools using `discover_tool` and `execute_tool`
-- The system uses a multi-agent architecture built with [LanggraphJS](https://github.com/langchain-ai/langgraphjs) for LLM logic and [Vercel AI SDK UI](https://sdk.vercel.ai/docs/ai-sdk-ui/overview) for frontend chatbot.
-- It uses a ["supervisor" architecture](https://langchain-ai.lang.chat/langgraphjs/tutorials/multi_agent/agent_supervisor/) that interacts with different agents for specialized tasks. For example, `findings_agent` can analyze detected security findings, while `overview_agent` provides a summary of connected cloud accounts.
-- The system connects to the configured LLM provider to understand user's query, fetches the right data, and responds to the query.
Lighthouse AI supports multiple LLM providers including OpenAI, Amazon Bedrock, and OpenAI-compatible services. For configuration details, see [Using Multiple LLM Providers with Lighthouse](/user-guide/tutorials/prowler-app-lighthouse-multi-llm).
-- The supervisor agent is the main contact point. It is what users interact with directly from the chat interface. It coordinates with other agents to answer users' questions comprehensively.
-
+
+
+
-All agents can only read relevant security data. They cannot modify your data or access sensitive information like configured secrets or tenant details.
+Lighthouse AI can only read relevant security data. It cannot modify data or access sensitive information such as configured secrets or tenant details.
-## Set up
+## Set Up
Getting started with Prowler Lighthouse AI is easy:
@@ -43,11 +52,11 @@ For detailed configuration instructions for each provider, see [Using Multiple L
### Adding Business Context
-The optional business context field lets you provide additional information to help Lighthouse AI understand your environment and priorities, including:
+The optional business context field lets teams provide additional information to help Lighthouse AI understand environment priorities, including:
-- Your organization's cloud security goals
+- Organization cloud security goals
- Information about account owners or responsible teams
-- Compliance requirements for your organization
+- Compliance requirements
- Current security initiatives or focus areas
Better context leads to more relevant responses and prioritization that aligns with your needs.
diff --git a/mcp_server/AGENTS.md b/mcp_server/AGENTS.md
index fdc1ce5535..bbefa2db57 100644
--- a/mcp_server/AGENTS.md
+++ b/mcp_server/AGENTS.md
@@ -1,310 +1,88 @@
# Prowler MCP Server - AI Agent Ruleset
-**Complete guide for AI agents and developers working on the Prowler MCP Server - the Model Context Protocol server that provides AI agents access to the Prowler ecosystem.**
+> **Skills Reference**: For detailed patterns, use the [`prowler-mcp`](../skills/prowler-mcp/SKILL.md) skill.
## Project Overview
-The Prowler MCP Server brings the entire Prowler ecosystem to AI assistants through
-the Model Context Protocol (MCP). It enables seamless integration with AI tools
-like Claude Desktop, Cursor, and other MCP hosts, allowing interaction with
-Prowler's security capabilities through natural language.
+The Prowler MCP Server provides AI agents access to the Prowler ecosystem through the Model Context Protocol (MCP). It integrates with Claude Desktop, Cursor, and other MCP hosts.
---
-## Critical Rules
+## CRITICAL RULES
### Tool Implementation
-
-- **ALWAYS**: Extend `BaseTool` ABC for new Prowler App tools (auto-registration)
-- **ALWAYS**: Use `@mcp.tool()` decorator for Hub/Docs tools (manual registration)
-- **NEVER**: Manually register BaseTool subclasses (auto-discovered via `load_all_tools()`)
-- **NEVER**: Import tools directly in server.py (tool_loader handles discovery)
+- ALWAYS: Extend `BaseTool` ABC for Prowler App tools (auto-registration)
+- ALWAYS: Use `@mcp.tool()` decorator for Hub/Docs tools
+- NEVER: Manually register BaseTool subclasses
+- NEVER: Import tools directly in server.py
### Models
-
-- **ALWAYS**: Use `MinimalSerializerMixin` for LLM-optimized responses
-- **ALWAYS**: Implement `from_api_response()` factory method for API transformations
-- **ALWAYS**: Use two-tier models (Simplified for lists, Detailed for single items)
-- **NEVER**: Return raw API responses (transform to simplified models)
+- ALWAYS: Use `MinimalSerializerMixin` for LLM-optimized responses
+- ALWAYS: Implement `from_api_response()` factory method
+- ALWAYS: Two-tier models (Simplified for lists, Detailed for single items)
+- NEVER: Return raw API responses
### API Client
-
-- **ALWAYS**: Use singleton `ProwlerAPIClient` via `self.api_client` in tools
-- **ALWAYS**: Use `build_filter_params()` for query parameter normalization
-- **NEVER**: Create new httpx clients in tools (use shared client)
+- ALWAYS: Use singleton `ProwlerAPIClient` via `self.api_client`
+- ALWAYS: Use `build_filter_params()` for query parameters
+- NEVER: Create new httpx clients in tools
---
-## Architecture
+## ARCHITECTURE
-### Three Sub-Servers Pattern
-
-The main server (`server.py`) orchestrates three independent sub-servers with prefixed tool namespacing:
+### Three Sub-Servers
```python
-# server.py imports sub-servers with prefixes
await prowler_mcp_server.import_server(hub_mcp_server, prefix="prowler_hub")
await prowler_mcp_server.import_server(app_mcp_server, prefix="prowler_app")
await prowler_mcp_server.import_server(docs_mcp_server, prefix="prowler_docs")
```
-This pattern ensures:
-- Failures in one sub-server do not block others
-- Clear tool namespacing for LLM disambiguation
-- Independent development and testing
-
-### Tool Naming Convention
-
-All tools follow a consistent naming pattern with prefixes:
-- `prowler_hub_*` - Prowler Hub catalog and compliance tools
-- `prowler_docs_*` - Prowler documentation search and retrieval
-- `prowler_app_*` - Prowler Cloud and App (Self-Managed) management tools
-
-### Tool Registration Patterns
-
-**Pattern 1: Prowler Hub/Docs (Direct Decorators)**
-
-```python
-# prowler_hub/server.py or prowler_documentation/server.py
-hub_mcp_server = FastMCP("prowler-hub")
-
-@hub_mcp_server.tool()
-async def get_checks(providers: str | None = None) -> dict:
- """Tool docstring becomes LLM description."""
- # Direct implementation
- response = prowler_hub_client.get("/check", params=params)
- return response.json()
-```
-
-**Pattern 2: Prowler App (BaseTool Auto-Registration)**
-
-```python
-# prowler_app/tools/findings.py
-class FindingsTools(BaseTool):
- async def search_security_findings(
- self,
- severity: list[str] = Field(default=[], description="Filter by severity")
- ) -> dict:
- """Docstring becomes LLM description."""
- response = await self.api_client.get("/api/v1/findings")
- return SimplifiedFinding.from_api_response(response).model_dump()
-```
-
-NOTE: Only public methods of `BaseTool` subclasses are registered as tools.
+### Tool Naming
+- `prowler_hub_*` - Catalog and compliance (no auth)
+- `prowler_docs_*` - Documentation search (no auth)
+- `prowler_app_*` - Cloud/App management (auth required)
---
-## Tech Stack
+## TECH STACK
-- **Language**: Python 3.12+
-- **MCP Framework**: FastMCP 2.13.1
-- **HTTP Client**: httpx (async)
-- **Validation**: Pydantic with MinimalSerializerMixin
-- **Package Manager**: uv
+Python 3.12+ | FastMCP 2.13.1 | httpx (async) | Pydantic | uv
---
-## Project Structure
+## PROJECT STRUCTURE
```
-mcp_server/
-├── README.md # User documentation
-├── AGENTS.md # This file - AI agent guidelines
-├── CHANGELOG.md # Version history
-├── pyproject.toml # Project metadata and dependencies
-├── Dockerfile # Container image definition
-├── entrypoint.sh # Docker entrypoint script
-└── prowler_mcp_server/
- ├── __init__.py # Version info
- ├── main.py # CLI entry point
- ├── server.py # Main FastMCP server orchestration
- ├── lib/
- │ └── logger.py # Structured logging
- ├── prowler_hub/
- │ └── server.py # Hub tools (10 tools, no auth)
- ├── prowler_app/
- │ ├── server.py # App server initialization
- │ ├── tools/
- │ │ ├── base.py # BaseTool abstract class
- │ │ ├── findings.py # Findings tools
- │ │ ├── providers.py # Provider tools
- │ │ ├── scans.py # Scan tools
- │ │ ├── resources.py # Resource tools
- │ │ └── muting.py # Muting tools
- │ ├── models/
- │ │ ├── base.py # MinimalSerializerMixin
- │ │ ├── findings.py # Finding models
- │ │ ├── providers.py # Provider models
- │ │ ├── scans.py # Scan models
- │ │ ├── resources.py # Resource models
- │ │ └── muting.py # Muting models
- │ └── utils/
- │ ├── api_client.py # ProwlerAPIClient singleton
- │ ├── auth.py # ProwlerAppAuth (STDIO/HTTP)
- │ └── tool_loader.py # Auto-discovery and registration
- └── prowler_documentation/
- ├── server.py # Documentation tools (2 tools, no auth)
- └── search_engine.py # Mintlify API integration
+mcp_server/prowler_mcp_server/
+├── server.py # Main orchestration
+├── prowler_hub/server.py # Hub tools (no auth)
+├── prowler_app/
+│ ├── server.py
+│ ├── tools/{feature}.py # BaseTool subclasses
+│ ├── models/{feature}.py # Pydantic models
+│ └── utils/api_client.py # ProwlerAPIClient
+└── prowler_documentation/
+ └── server.py # Docs tools (no auth)
```
---
-## Commands
-
-NOTE: To run a python command always use `uv run ` from within the `mcp_server/` directory.
-
-### Development
+## COMMANDS
```bash
-# Navigate to MCP server directory
-cd mcp_server
-
-# Run in STDIO mode (default)
-uv run prowler-mcp
-
-# Run in HTTP mode
-uv run prowler-mcp --transport http --host 0.0.0.0 --port 8000
-
-# Run from anywhere using uvx
-uvx /path/to/prowler/mcp_server/
+cd mcp_server && uv run prowler-mcp # STDIO mode
+cd mcp_server && uv run prowler-mcp --transport http --port 8000 # HTTP mode
```
---
-## Development Patterns
+## QA CHECKLIST
-### Adding New Tools to Prowler App
-
-1. **Create or extend a tool class** in `prowler_app/tools/`:
-
-```python
-# prowler_app/tools/new_feature.py
-from pydantic import Field
-from prowler_mcp_server.prowler_app.tools.base import BaseTool
-from prowler_mcp_server.prowler_app.models.new_feature import FeatureResponse
-
-class NewFeatureTools(BaseTool):
- async def list_features(
- self,
- status: str | None = Field(default=None, description="Filter by status")
- ) -> dict:
- """List all features with optional filtering.
-
- Returns a simplified list of features optimized for LLM consumption.
- """
- params = {}
- if status:
- params["filter[status]"] = status
-
- clean_params = self.api_client.build_filter_params(params)
- response = await self.api_client.get("/api/v1/features", params=clean_params)
-
- return FeatureResponse.from_api_response(response).model_dump()
-```
-
-2. **Create corresponding models** in `prowler_app/models/`:
-
-```python
-# prowler_app/models/new_feature.py
-from pydantic import Field
-from prowler_mcp_server.prowler_app.models.base import MinimalSerializerMixin
-
-class SimplifiedFeature(MinimalSerializerMixin):
- """Lightweight feature for list operations."""
- id: str
- name: str
- status: str
-
-class DetailedFeature(SimplifiedFeature):
- """Extended feature with complete details."""
- description: str | None = None
- created_at: str
- updated_at: str
-
- @classmethod
- def from_api_response(cls, data: dict) -> "DetailedFeature":
- """Transform API response to model."""
- attributes = data.get("attributes", {})
- return cls(
- id=data["id"],
- name=attributes["name"],
- status=attributes["status"],
- description=attributes.get("description"),
- created_at=attributes["created_at"],
- updated_at=attributes["updated_at"],
- )
-```
-
-3. **No registration needed** - the tool loader auto-discovers BaseTool subclasses
-
-### Adding Tools to Prowler Hub/Docs
-
-Use the `@mcp.tool()` decorator directly:
-
-```python
-# prowler_hub/server.py
-@hub_mcp_server.tool()
-async def new_hub_tool(param: str) -> dict:
- """Tool description for LLM."""
- response = prowler_hub_client.get("/endpoint")
- return response.json()
-```
-
----
-
-## Code Quality Standards
-
-### Tool Docstrings
-
-Tool docstrings become AI agent descriptions. Write them in a clear, concise manner focusing on LLM-relevant behavior:
-
-```python
-async def search_security_findings(
- self,
- severity: list[str] = Field(default=[], description="Filter by severity levels")
-) -> dict:
- """Search security findings with advanced filtering.
-
- Returns a lightweight list of findings optimized for LLM consumption.
- Use get_finding_details for complete information about a specific finding.
- """
-```
-
-### Model Design
-
-- Use `MinimalSerializerMixin` to exclude None/empty values
-- Implement `from_api_response()` for consistent API transformation
-- Create two-tier models: Simplified (lists) and Detailed (single items)
-
-### Error Handling
-
-Return structured error responses rather than raising exceptions:
-
-```python
-try:
- response = await self.api_client.get(f"/api/v1/items/{item_id}")
- return DetailedItem.from_api_response(response["data"]).model_dump()
-except Exception as e:
- self.logger.error(f"Failed to get item {item_id}: {e}")
- return {"error": str(e), "status": "failed"}
-```
-
----
-
-## QA Checklist Before Commit
-
-- [ ] Tool docstrings are clear and describe LLM-relevant behavior
-- [ ] Models use `MinimalSerializerMixin` for LLM optimization
-- [ ] API responses are transformed to simplified models
-- [ ] No hardcoded secrets or API keys
+- [ ] Tool docstrings describe LLM-relevant behavior
+- [ ] Models use `MinimalSerializerMixin`
+- [ ] API responses transformed to simplified models
+- [ ] No hardcoded secrets
- [ ] Error handling returns structured responses
-- [ ] New tools are auto-discovered (BaseTool subclass) or properly decorated
-- [ ] Parameter descriptions use Pydantic `Field()` with clear descriptions
-
----
-
-## References
-
-- **Root Project Guide**: `../AGENTS.md`
-- **FastMCP Documentation**: https://gofastmcp.com/llms.txt
-- **Prowler API Documentation**: https://api.prowler.com/api/v1/docs
+- [ ] Parameter descriptions use Pydantic `Field()`
diff --git a/mcp_server/pyproject.toml b/mcp_server/pyproject.toml
index cd6e674e1b..0db6ef67f7 100644
--- a/mcp_server/pyproject.toml
+++ b/mcp_server/pyproject.toml
@@ -4,7 +4,7 @@ requires = ["setuptools>=61.0", "wheel"]
[project]
dependencies = [
- "fastmcp==2.13.1",
+ "fastmcp==2.14.0",
"httpx>=0.28.0"
]
description = "MCP server for Prowler ecosystem"
diff --git a/permissions/create_role_to_assume_cfn.yaml b/permissions/create_role_to_assume_cfn.yaml
deleted file mode 100644
index 0f41421808..0000000000
--- a/permissions/create_role_to_assume_cfn.yaml
+++ /dev/null
@@ -1,120 +0,0 @@
-AWSTemplateFormatVersion: '2010-09-09'
-#
-# You can invoke CloudFormation and pass the principal ARN from a command line like this:
-# aws cloudformation create-stack \
-# --capabilities CAPABILITY_IAM --capabilities CAPABILITY_NAMED_IAM \
-# --template-body "file://create_role_to_assume_cfn.yaml" \
-# --stack-name "ProwlerScanRole" \
-# --parameters "ParameterKey=AuthorisedARN,ParameterValue=arn:aws:iam::123456789012:root" \
-# "ParameterKey=ExternalId,ParameterValue=YourExternalId"
-#
-Description: |
- This template creates an AWS IAM Role with an inline policy and two AWS managed policies
- attached. It sets the trust policy on that IAM Role to permit a named ARN in another AWS
- account to assume that role. The role name and the ARN of the trusted user can all be passed
- to the CloudFormation stack as parameters. Then you can run Prowler to perform a security
- assessment with a command like:
- prowler --role ProwlerScanRole.ARN
-Parameters:
- AuthorisedARN:
- Description: |
- ARN of user who is authorised to assume the role that is created by this template.
- E.g., arn:aws:iam::123456789012:root
- Type: String
- ExternalId:
- Description: |
- This is the External ID that Prowler will use to assume the role.
- Type: String
- MinLength: 1
- AllowedPattern: ".+"
- ConstraintDescription: "ExternalId must not be empty."
- ProwlerRoleName:
- Description: |
- Name of the IAM role that will have these policies attached. Default: ProwlerScanRole
- Type: String
- Default: 'ProwlerScanRole'
-
-Resources:
- ProwlerScanRole:
- Type: AWS::IAM::Role
- Properties:
- AssumeRolePolicyDocument:
- Version: '2012-10-17'
- Statement:
- - Effect: Allow
- Principal:
- AWS: !Sub ${AuthorisedARN}
- Action: 'sts:AssumeRole'
- Condition:
- StringEquals:
- 'sts:ExternalId': !Ref ExternalId
- ## In case MFA is required uncomment lines below and read https://github.com/prowler-cloud/prowler#run-prowler-with-mfa-protected-credentials
- # Bool:
- # 'aws:MultiFactorAuthPresent': true
- # This is 12h that is maximum allowed, Minimum is 3600 = 1h
- # to take advantage of this use -T like in './prowler --role ProwlerScanRole.ARN -T 43200'
- MaxSessionDuration: 43200
- ManagedPolicyArns:
- - 'arn:aws:iam::aws:policy/SecurityAudit'
- - 'arn:aws:iam::aws:policy/job-function/ViewOnlyAccess'
- RoleName: !Sub ${ProwlerRoleName}
- Policies:
- - PolicyName: ProwlerScanRoleAdditionalViewPrivileges
- PolicyDocument:
- Version : '2012-10-17'
- Statement:
- - Effect: Allow
- Action:
- - 'account:Get*'
- - 'appstream:Describe*'
- - 'appstream:List*'
- - 'backup:List*'
- - 'bedrock:List*'
- - 'bedrock:Get*'
- - 'cloudtrail:GetInsightSelectors'
- - 'codeartifact:List*'
- - 'codebuild:BatchGet*'
- - 'codebuild:ListReportGroups'
- - 'cognito-idp:GetUserPoolMfaConfig'
- - 'dlm:Get*'
- - 'drs:Describe*'
- - 'ds:Get*'
- - 'ds:Describe*'
- - 'ds:List*'
- - 'dynamodb:GetResourcePolicy'
- - 'ec2:GetEbsEncryptionByDefault'
- - 'ec2:GetSnapshotBlockPublicAccessState'
- - 'ec2:GetInstanceMetadataDefaults'
- - 'ecr:Describe*'
- - 'ecr:GetRegistryScanningConfiguration'
- - 'elasticfilesystem:DescribeBackupPolicy'
- - 'glue:GetConnections'
- - 'glue:GetSecurityConfiguration*'
- - 'glue:SearchTables'
- - 'lambda:GetFunction*'
- - 'logs:FilterLogEvents'
- - 'lightsail:GetRelationalDatabases'
- - 'macie2:GetMacieSession'
- - 'macie2:GetAutomatedDiscoveryConfiguration'
- - 's3:GetAccountPublicAccessBlock'
- - 'shield:DescribeProtection'
- - 'shield:GetSubscriptionState'
- - 'securityhub:BatchImportFindings'
- - 'securityhub:GetFindings'
- - 'servicecatalog:Describe*'
- - 'servicecatalog:List*'
- - 'ssm:GetDocument'
- - 'ssm-incidents:List*'
- - 'states:ListTagsForResource'
- - 'support:Describe*'
- - 'tag:GetTagKeys'
- - 'wellarchitected:List*'
- Resource: '*'
- - PolicyName: ProwlerScanRoleAdditionalViewPrivilegesApiGateway
- PolicyDocument:
- Version : '2012-10-17'
- Statement:
- - Effect: Allow
- Action:
- - 'apigateway:GET'
- Resource: 'arn:aws:apigateway:*::/restapis/*'
diff --git a/poetry.lock b/poetry.lock
index 5e4ddb6b3f..3189aa7b0f 100644
--- a/poetry.lock
+++ b/poetry.lock
@@ -38,98 +38,132 @@ files = [
[[package]]
name = "aiohttp"
-version = "3.12.14"
+version = "3.13.3"
description = "Async http client/server framework (asyncio)"
optional = false
python-versions = ">=3.9"
groups = ["main"]
files = [
- {file = "aiohttp-3.12.14-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:906d5075b5ba0dd1c66fcaaf60eb09926a9fef3ca92d912d2a0bbdbecf8b1248"},
- {file = "aiohttp-3.12.14-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c875bf6fc2fd1a572aba0e02ef4e7a63694778c5646cdbda346ee24e630d30fb"},
- {file = "aiohttp-3.12.14-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fbb284d15c6a45fab030740049d03c0ecd60edad9cd23b211d7e11d3be8d56fd"},
- {file = "aiohttp-3.12.14-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:38e360381e02e1a05d36b223ecab7bc4a6e7b5ab15760022dc92589ee1d4238c"},
- {file = "aiohttp-3.12.14-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:aaf90137b5e5d84a53632ad95ebee5c9e3e7468f0aab92ba3f608adcb914fa95"},
- {file = "aiohttp-3.12.14-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e532a25e4a0a2685fa295a31acf65e027fbe2bea7a4b02cdfbbba8a064577663"},
- {file = "aiohttp-3.12.14-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:eab9762c4d1b08ae04a6c77474e6136da722e34fdc0e6d6eab5ee93ac29f35d1"},
- {file = "aiohttp-3.12.14-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:abe53c3812b2899889a7fca763cdfaeee725f5be68ea89905e4275476ffd7e61"},
- {file = "aiohttp-3.12.14-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5760909b7080aa2ec1d320baee90d03b21745573780a072b66ce633eb77a8656"},
- {file = "aiohttp-3.12.14-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:02fcd3f69051467bbaa7f84d7ec3267478c7df18d68b2e28279116e29d18d4f3"},
- {file = "aiohttp-3.12.14-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:4dcd1172cd6794884c33e504d3da3c35648b8be9bfa946942d353b939d5f1288"},
- {file = "aiohttp-3.12.14-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:224d0da41355b942b43ad08101b1b41ce633a654128ee07e36d75133443adcda"},
- {file = "aiohttp-3.12.14-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:e387668724f4d734e865c1776d841ed75b300ee61059aca0b05bce67061dcacc"},
- {file = "aiohttp-3.12.14-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:dec9cde5b5a24171e0b0a4ca064b1414950904053fb77c707efd876a2da525d8"},
- {file = "aiohttp-3.12.14-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:bbad68a2af4877cc103cd94af9160e45676fc6f0c14abb88e6e092b945c2c8e3"},
- {file = "aiohttp-3.12.14-cp310-cp310-win32.whl", hash = "sha256:ee580cb7c00bd857b3039ebca03c4448e84700dc1322f860cf7a500a6f62630c"},
- {file = "aiohttp-3.12.14-cp310-cp310-win_amd64.whl", hash = "sha256:cf4f05b8cea571e2ccc3ca744e35ead24992d90a72ca2cf7ab7a2efbac6716db"},
- {file = "aiohttp-3.12.14-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:f4552ff7b18bcec18b60a90c6982049cdb9dac1dba48cf00b97934a06ce2e597"},
- {file = "aiohttp-3.12.14-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:8283f42181ff6ccbcf25acaae4e8ab2ff7e92b3ca4a4ced73b2c12d8cd971393"},
- {file = "aiohttp-3.12.14-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:040afa180ea514495aaff7ad34ec3d27826eaa5d19812730fe9e529b04bb2179"},
- {file = "aiohttp-3.12.14-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b413c12f14c1149f0ffd890f4141a7471ba4b41234fe4fd4a0ff82b1dc299dbb"},
- {file = "aiohttp-3.12.14-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:1d6f607ce2e1a93315414e3d448b831238f1874b9968e1195b06efaa5c87e245"},
- {file = "aiohttp-3.12.14-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:565e70d03e924333004ed101599902bba09ebb14843c8ea39d657f037115201b"},
- {file = "aiohttp-3.12.14-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4699979560728b168d5ab63c668a093c9570af2c7a78ea24ca5212c6cdc2b641"},
- {file = "aiohttp-3.12.14-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ad5fdf6af93ec6c99bf800eba3af9a43d8bfd66dce920ac905c817ef4a712afe"},
- {file = "aiohttp-3.12.14-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4ac76627c0b7ee0e80e871bde0d376a057916cb008a8f3ffc889570a838f5cc7"},
- {file = "aiohttp-3.12.14-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:798204af1180885651b77bf03adc903743a86a39c7392c472891649610844635"},
- {file = "aiohttp-3.12.14-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:4f1205f97de92c37dd71cf2d5bcfb65fdaed3c255d246172cce729a8d849b4da"},
- {file = "aiohttp-3.12.14-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:76ae6f1dd041f85065d9df77c6bc9c9703da9b5c018479d20262acc3df97d419"},
- {file = "aiohttp-3.12.14-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:a194ace7bc43ce765338ca2dfb5661489317db216ea7ea700b0332878b392cab"},
- {file = "aiohttp-3.12.14-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:16260e8e03744a6fe3fcb05259eeab8e08342c4c33decf96a9dad9f1187275d0"},
- {file = "aiohttp-3.12.14-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:8c779e5ebbf0e2e15334ea404fcce54009dc069210164a244d2eac8352a44b28"},
- {file = "aiohttp-3.12.14-cp311-cp311-win32.whl", hash = "sha256:a289f50bf1bd5be227376c067927f78079a7bdeccf8daa6a9e65c38bae14324b"},
- {file = "aiohttp-3.12.14-cp311-cp311-win_amd64.whl", hash = "sha256:0b8a69acaf06b17e9c54151a6c956339cf46db4ff72b3ac28516d0f7068f4ced"},
- {file = "aiohttp-3.12.14-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:a0ecbb32fc3e69bc25efcda7d28d38e987d007096cbbeed04f14a6662d0eee22"},
- {file = "aiohttp-3.12.14-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:0400f0ca9bb3e0b02f6466421f253797f6384e9845820c8b05e976398ac1d81a"},
- {file = "aiohttp-3.12.14-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a56809fed4c8a830b5cae18454b7464e1529dbf66f71c4772e3cfa9cbec0a1ff"},
- {file = "aiohttp-3.12.14-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:27f2e373276e4755691a963e5d11756d093e346119f0627c2d6518208483fb6d"},
- {file = "aiohttp-3.12.14-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:ca39e433630e9a16281125ef57ece6817afd1d54c9f1bf32e901f38f16035869"},
- {file = "aiohttp-3.12.14-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9c748b3f8b14c77720132b2510a7d9907a03c20ba80f469e58d5dfd90c079a1c"},
- {file = "aiohttp-3.12.14-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f0a568abe1b15ce69d4cc37e23020720423f0728e3cb1f9bcd3f53420ec3bfe7"},
- {file = "aiohttp-3.12.14-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9888e60c2c54eaf56704b17feb558c7ed6b7439bca1e07d4818ab878f2083660"},
- {file = "aiohttp-3.12.14-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3006a1dc579b9156de01e7916d38c63dc1ea0679b14627a37edf6151bc530088"},
- {file = "aiohttp-3.12.14-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:aa8ec5c15ab80e5501a26719eb48a55f3c567da45c6ea5bb78c52c036b2655c7"},
- {file = "aiohttp-3.12.14-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:39b94e50959aa07844c7fe2206b9f75d63cc3ad1c648aaa755aa257f6f2498a9"},
- {file = "aiohttp-3.12.14-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:04c11907492f416dad9885d503fbfc5dcb6768d90cad8639a771922d584609d3"},
- {file = "aiohttp-3.12.14-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:88167bd9ab69bb46cee91bd9761db6dfd45b6e76a0438c7e884c3f8160ff21eb"},
- {file = "aiohttp-3.12.14-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:791504763f25e8f9f251e4688195e8b455f8820274320204f7eafc467e609425"},
- {file = "aiohttp-3.12.14-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2785b112346e435dd3a1a67f67713a3fe692d288542f1347ad255683f066d8e0"},
- {file = "aiohttp-3.12.14-cp312-cp312-win32.whl", hash = "sha256:15f5f4792c9c999a31d8decf444e79fcfd98497bf98e94284bf390a7bb8c1729"},
- {file = "aiohttp-3.12.14-cp312-cp312-win_amd64.whl", hash = "sha256:3b66e1a182879f579b105a80d5c4bd448b91a57e8933564bf41665064796a338"},
- {file = "aiohttp-3.12.14-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:3143a7893d94dc82bc409f7308bc10d60285a3cd831a68faf1aa0836c5c3c767"},
- {file = "aiohttp-3.12.14-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:3d62ac3d506cef54b355bd34c2a7c230eb693880001dfcda0bf88b38f5d7af7e"},
- {file = "aiohttp-3.12.14-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:48e43e075c6a438937c4de48ec30fa8ad8e6dfef122a038847456bfe7b947b63"},
- {file = "aiohttp-3.12.14-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:077b4488411a9724cecc436cbc8c133e0d61e694995b8de51aaf351c7578949d"},
- {file = "aiohttp-3.12.14-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:d8c35632575653f297dcbc9546305b2c1133391089ab925a6a3706dfa775ccab"},
- {file = "aiohttp-3.12.14-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6b8ce87963f0035c6834b28f061df90cf525ff7c9b6283a8ac23acee6502afd4"},
- {file = "aiohttp-3.12.14-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f0a2cf66e32a2563bb0766eb24eae7e9a269ac0dc48db0aae90b575dc9583026"},
- {file = "aiohttp-3.12.14-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cdea089caf6d5cde975084a884c72d901e36ef9c2fd972c9f51efbbc64e96fbd"},
- {file = "aiohttp-3.12.14-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8a7865f27db67d49e81d463da64a59365ebd6b826e0e4847aa111056dcb9dc88"},
- {file = "aiohttp-3.12.14-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0ab5b38a6a39781d77713ad930cb5e7feea6f253de656a5f9f281a8f5931b086"},
- {file = "aiohttp-3.12.14-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:9b3b15acee5c17e8848d90a4ebc27853f37077ba6aec4d8cb4dbbea56d156933"},
- {file = "aiohttp-3.12.14-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:e4c972b0bdaac167c1e53e16a16101b17c6d0ed7eac178e653a07b9f7fad7151"},
- {file = "aiohttp-3.12.14-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:7442488b0039257a3bdbc55f7209587911f143fca11df9869578db6c26feeeb8"},
- {file = "aiohttp-3.12.14-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:f68d3067eecb64c5e9bab4a26aa11bd676f4c70eea9ef6536b0a4e490639add3"},
- {file = "aiohttp-3.12.14-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f88d3704c8b3d598a08ad17d06006cb1ca52a1182291f04979e305c8be6c9758"},
- {file = "aiohttp-3.12.14-cp313-cp313-win32.whl", hash = "sha256:a3c99ab19c7bf375c4ae3debd91ca5d394b98b6089a03231d4c580ef3c2ae4c5"},
- {file = "aiohttp-3.12.14-cp313-cp313-win_amd64.whl", hash = "sha256:3f8aad695e12edc9d571f878c62bedc91adf30c760c8632f09663e5f564f4baa"},
- {file = "aiohttp-3.12.14-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:b8cc6b05e94d837bcd71c6531e2344e1ff0fb87abe4ad78a9261d67ef5d83eae"},
- {file = "aiohttp-3.12.14-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:d1dcb015ac6a3b8facd3677597edd5ff39d11d937456702f0bb2b762e390a21b"},
- {file = "aiohttp-3.12.14-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3779ed96105cd70ee5e85ca4f457adbce3d9ff33ec3d0ebcdf6c5727f26b21b3"},
- {file = "aiohttp-3.12.14-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:717a0680729b4ebd7569c1dcd718c46b09b360745fd8eb12317abc74b14d14d0"},
- {file = "aiohttp-3.12.14-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:b5dd3a2ef7c7e968dbbac8f5574ebeac4d2b813b247e8cec28174a2ba3627170"},
- {file = "aiohttp-3.12.14-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4710f77598c0092239bc12c1fcc278a444e16c7032d91babf5abbf7166463f7b"},
- {file = "aiohttp-3.12.14-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f3e9f75ae842a6c22a195d4a127263dbf87cbab729829e0bd7857fb1672400b2"},
- {file = "aiohttp-3.12.14-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5f9c8d55d6802086edd188e3a7d85a77787e50d56ce3eb4757a3205fa4657922"},
- {file = "aiohttp-3.12.14-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:79b29053ff3ad307880d94562cca80693c62062a098a5776ea8ef5ef4b28d140"},
- {file = "aiohttp-3.12.14-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:23e1332fff36bebd3183db0c7a547a1da9d3b4091509f6d818e098855f2f27d3"},
- {file = "aiohttp-3.12.14-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:a564188ce831fd110ea76bcc97085dd6c625b427db3f1dbb14ca4baa1447dcbc"},
- {file = "aiohttp-3.12.14-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:a7a1b4302f70bb3ec40ca86de82def532c97a80db49cac6a6700af0de41af5ee"},
- {file = "aiohttp-3.12.14-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:1b07ccef62950a2519f9bfc1e5b294de5dd84329f444ca0b329605ea787a3de5"},
- {file = "aiohttp-3.12.14-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:938bd3ca6259e7e48b38d84f753d548bd863e0c222ed6ee6ace3fd6752768a84"},
- {file = "aiohttp-3.12.14-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:8bc784302b6b9f163b54c4e93d7a6f09563bd01ff2b841b29ed3ac126e5040bf"},
- {file = "aiohttp-3.12.14-cp39-cp39-win32.whl", hash = "sha256:a3416f95961dd7d5393ecff99e3f41dc990fb72eda86c11f2a60308ac6dcd7a0"},
- {file = "aiohttp-3.12.14-cp39-cp39-win_amd64.whl", hash = "sha256:196858b8820d7f60578f8b47e5669b3195c21d8ab261e39b1d705346458f445f"},
- {file = "aiohttp-3.12.14.tar.gz", hash = "sha256:6e06e120e34d93100de448fd941522e11dafa78ef1a893c179901b7d66aa29f2"},
+ {file = "aiohttp-3.13.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d5a372fd5afd301b3a89582817fdcdb6c34124787c70dbcc616f259013e7eef7"},
+ {file = "aiohttp-3.13.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:147e422fd1223005c22b4fe080f5d93ced44460f5f9c105406b753612b587821"},
+ {file = "aiohttp-3.13.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:859bd3f2156e81dd01432f5849fc73e2243d4a487c4fd26609b1299534ee1845"},
+ {file = "aiohttp-3.13.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dca68018bf48c251ba17c72ed479f4dafe9dbd5a73707ad8d28a38d11f3d42af"},
+ {file = "aiohttp-3.13.3-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:fee0c6bc7db1de362252affec009707a17478a00ec69f797d23ca256e36d5940"},
+ {file = "aiohttp-3.13.3-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c048058117fd649334d81b4b526e94bde3ccaddb20463a815ced6ecbb7d11160"},
+ {file = "aiohttp-3.13.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:215a685b6fbbfcf71dfe96e3eba7a6f58f10da1dfdf4889c7dd856abe430dca7"},
+ {file = "aiohttp-3.13.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:de2c184bb1fe2cbd2cefba613e9db29a5ab559323f994b6737e370d3da0ac455"},
+ {file = "aiohttp-3.13.3-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:75ca857eba4e20ce9f546cd59c7007b33906a4cd48f2ff6ccf1ccfc3b646f279"},
+ {file = "aiohttp-3.13.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:81e97251d9298386c2b7dbeb490d3d1badbdc69107fb8c9299dd04eb39bddc0e"},
+ {file = "aiohttp-3.13.3-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:c0e2d366af265797506f0283487223146af57815b388623f0357ef7eac9b209d"},
+ {file = "aiohttp-3.13.3-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:4e239d501f73d6db1522599e14b9b321a7e3b1de66ce33d53a765d975e9f4808"},
+ {file = "aiohttp-3.13.3-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:0db318f7a6f065d84cb1e02662c526294450b314a02bd9e2a8e67f0d8564ce40"},
+ {file = "aiohttp-3.13.3-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:bfc1cc2fe31a6026a8a88e4ecfb98d7f6b1fec150cfd708adbfd1d2f42257c29"},
+ {file = "aiohttp-3.13.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:af71fff7bac6bb7508956696dce8f6eec2bbb045eceb40343944b1ae62b5ef11"},
+ {file = "aiohttp-3.13.3-cp310-cp310-win32.whl", hash = "sha256:37da61e244d1749798c151421602884db5270faf479cf0ef03af0ff68954c9dd"},
+ {file = "aiohttp-3.13.3-cp310-cp310-win_amd64.whl", hash = "sha256:7e63f210bc1b57ef699035f2b4b6d9ce096b5914414a49b0997c839b2bd2223c"},
+ {file = "aiohttp-3.13.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:5b6073099fb654e0a068ae678b10feff95c5cae95bbfcbfa7af669d361a8aa6b"},
+ {file = "aiohttp-3.13.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1cb93e166e6c28716c8c6aeb5f99dfb6d5ccf482d29fe9bf9a794110e6d0ab64"},
+ {file = "aiohttp-3.13.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:28e027cf2f6b641693a09f631759b4d9ce9165099d2b5d92af9bd4e197690eea"},
+ {file = "aiohttp-3.13.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3b61b7169ababd7802f9568ed96142616a9118dd2be0d1866e920e77ec8fa92a"},
+ {file = "aiohttp-3.13.3-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:80dd4c21b0f6237676449c6baaa1039abae86b91636b6c91a7f8e61c87f89540"},
+ {file = "aiohttp-3.13.3-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:65d2ccb7eabee90ce0503c17716fc77226be026dcc3e65cce859a30db715025b"},
+ {file = "aiohttp-3.13.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5b179331a481cb5529fca8b432d8d3c7001cb217513c94cd72d668d1248688a3"},
+ {file = "aiohttp-3.13.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d4c940f02f49483b18b079d1c27ab948721852b281f8b015c058100e9421dd1"},
+ {file = "aiohttp-3.13.3-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f9444f105664c4ce47a2a7171a2418bce5b7bae45fb610f4e2c36045d85911d3"},
+ {file = "aiohttp-3.13.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:694976222c711d1d00ba131904beb60534f93966562f64440d0c9d41b8cdb440"},
+ {file = "aiohttp-3.13.3-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:f33ed1a2bf1997a36661874b017f5c4b760f41266341af36febaf271d179f6d7"},
+ {file = "aiohttp-3.13.3-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:e636b3c5f61da31a92bf0d91da83e58fdfa96f178ba682f11d24f31944cdd28c"},
+ {file = "aiohttp-3.13.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:5d2d94f1f5fcbe40838ac51a6ab5704a6f9ea42e72ceda48de5e6b898521da51"},
+ {file = "aiohttp-3.13.3-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:2be0e9ccf23e8a94f6f0650ce06042cefc6ac703d0d7ab6c7a917289f2539ad4"},
+ {file = "aiohttp-3.13.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9af5e68ee47d6534d36791bbe9b646d2a7c7deb6fc24d7943628edfbb3581f29"},
+ {file = "aiohttp-3.13.3-cp311-cp311-win32.whl", hash = "sha256:a2212ad43c0833a873d0fb3c63fa1bacedd4cf6af2fee62bf4b739ceec3ab239"},
+ {file = "aiohttp-3.13.3-cp311-cp311-win_amd64.whl", hash = "sha256:642f752c3eb117b105acbd87e2c143de710987e09860d674e068c4c2c441034f"},
+ {file = "aiohttp-3.13.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:b903a4dfee7d347e2d87697d0713be59e0b87925be030c9178c5faa58ea58d5c"},
+ {file = "aiohttp-3.13.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a45530014d7a1e09f4a55f4f43097ba0fd155089372e105e4bff4ca76cb1b168"},
+ {file = "aiohttp-3.13.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:27234ef6d85c914f9efeb77ff616dbf4ad2380be0cda40b4db086ffc7ddd1b7d"},
+ {file = "aiohttp-3.13.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d32764c6c9aafb7fb55366a224756387cd50bfa720f32b88e0e6fa45b27dcf29"},
+ {file = "aiohttp-3.13.3-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:b1a6102b4d3ebc07dad44fbf07b45bb600300f15b552ddf1851b5390202ea2e3"},
+ {file = "aiohttp-3.13.3-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c014c7ea7fb775dd015b2d3137378b7be0249a448a1612268b5a90c2d81de04d"},
+ {file = "aiohttp-3.13.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2b8d8ddba8f95ba17582226f80e2de99c7a7948e66490ef8d947e272a93e9463"},
+ {file = "aiohttp-3.13.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9ae8dd55c8e6c4257eae3a20fd2c8f41edaea5992ed67156642493b8daf3cecc"},
+ {file = "aiohttp-3.13.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:01ad2529d4b5035578f5081606a465f3b814c542882804e2e8cda61adf5c71bf"},
+ {file = "aiohttp-3.13.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:bb4f7475e359992b580559e008c598091c45b5088f28614e855e42d39c2f1033"},
+ {file = "aiohttp-3.13.3-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:c19b90316ad3b24c69cd78d5c9b4f3aa4497643685901185b65166293d36a00f"},
+ {file = "aiohttp-3.13.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:96d604498a7c782cb15a51c406acaea70d8c027ee6b90c569baa6e7b93073679"},
+ {file = "aiohttp-3.13.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:084911a532763e9d3dd95adf78a78f4096cd5f58cdc18e6fdbc1b58417a45423"},
+ {file = "aiohttp-3.13.3-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:7a4a94eb787e606d0a09404b9c38c113d3b099d508021faa615d70a0131907ce"},
+ {file = "aiohttp-3.13.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:87797e645d9d8e222e04160ee32aa06bc5c163e8499f24db719e7852ec23093a"},
+ {file = "aiohttp-3.13.3-cp312-cp312-win32.whl", hash = "sha256:b04be762396457bef43f3597c991e192ee7da460a4953d7e647ee4b1c28e7046"},
+ {file = "aiohttp-3.13.3-cp312-cp312-win_amd64.whl", hash = "sha256:e3531d63d3bdfa7e3ac5e9b27b2dd7ec9df3206a98e0b3445fa906f233264c57"},
+ {file = "aiohttp-3.13.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:5dff64413671b0d3e7d5918ea490bdccb97a4ad29b3f311ed423200b2203e01c"},
+ {file = "aiohttp-3.13.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:87b9aab6d6ed88235aa2970294f496ff1a1f9adcd724d800e9b952395a80ffd9"},
+ {file = "aiohttp-3.13.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:425c126c0dc43861e22cb1c14ba4c8e45d09516d0a3ae0a3f7494b79f5f233a3"},
+ {file = "aiohttp-3.13.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7f9120f7093c2a32d9647abcaf21e6ad275b4fbec5b55969f978b1a97c7c86bf"},
+ {file = "aiohttp-3.13.3-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:697753042d57f4bf7122cab985bf15d0cef23c770864580f5af4f52023a56bd6"},
+ {file = "aiohttp-3.13.3-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6de499a1a44e7de70735d0b39f67c8f25eb3d91eb3103be99ca0fa882cdd987d"},
+ {file = "aiohttp-3.13.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:37239e9f9a7ea9ac5bf6b92b0260b01f8a22281996da609206a84df860bc1261"},
+ {file = "aiohttp-3.13.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f76c1e3fe7d7c8afad7ed193f89a292e1999608170dcc9751a7462a87dfd5bc0"},
+ {file = "aiohttp-3.13.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fc290605db2a917f6e81b0e1e0796469871f5af381ce15c604a3c5c7e51cb730"},
+ {file = "aiohttp-3.13.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4021b51936308aeea0367b8f006dc999ca02bc118a0cc78c303f50a2ff6afb91"},
+ {file = "aiohttp-3.13.3-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:49a03727c1bba9a97d3e93c9f93ca03a57300f484b6e935463099841261195d3"},
+ {file = "aiohttp-3.13.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:3d9908a48eb7416dc1f4524e69f1d32e5d90e3981e4e37eb0aa1cd18f9cfa2a4"},
+ {file = "aiohttp-3.13.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:2712039939ec963c237286113c68dbad80a82a4281543f3abf766d9d73228998"},
+ {file = "aiohttp-3.13.3-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:7bfdc049127717581866fa4708791220970ce291c23e28ccf3922c700740fdc0"},
+ {file = "aiohttp-3.13.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8057c98e0c8472d8846b9c79f56766bcc57e3e8ac7bfd510482332366c56c591"},
+ {file = "aiohttp-3.13.3-cp313-cp313-win32.whl", hash = "sha256:1449ceddcdbcf2e0446957863af03ebaaa03f94c090f945411b61269e2cb5daf"},
+ {file = "aiohttp-3.13.3-cp313-cp313-win_amd64.whl", hash = "sha256:693781c45a4033d31d4187d2436f5ac701e7bbfe5df40d917736108c1cc7436e"},
+ {file = "aiohttp-3.13.3-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:ea37047c6b367fd4bd632bff8077449b8fa034b69e812a18e0132a00fae6e808"},
+ {file = "aiohttp-3.13.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:6fc0e2337d1a4c3e6acafda6a78a39d4c14caea625124817420abceed36e2415"},
+ {file = "aiohttp-3.13.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c685f2d80bb67ca8c3837823ad76196b3694b0159d232206d1e461d3d434666f"},
+ {file = "aiohttp-3.13.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:48e377758516d262bde50c2584fc6c578af272559c409eecbdd2bae1601184d6"},
+ {file = "aiohttp-3.13.3-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:34749271508078b261c4abb1767d42b8d0c0cc9449c73a4df494777dc55f0687"},
+ {file = "aiohttp-3.13.3-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:82611aeec80eb144416956ec85b6ca45a64d76429c1ed46ae1b5f86c6e0c9a26"},
+ {file = "aiohttp-3.13.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2fff83cfc93f18f215896e3a190e8e5cb413ce01553901aca925176e7568963a"},
+ {file = "aiohttp-3.13.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bbe7d4cecacb439e2e2a8a1a7b935c25b812af7a5fd26503a66dadf428e79ec1"},
+ {file = "aiohttp-3.13.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b928f30fe49574253644b1ca44b1b8adbd903aa0da4b9054a6c20fc7f4092a25"},
+ {file = "aiohttp-3.13.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7b5e8fe4de30df199155baaf64f2fcd604f4c678ed20910db8e2c66dc4b11603"},
+ {file = "aiohttp-3.13.3-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:8542f41a62bcc58fc7f11cf7c90e0ec324ce44950003feb70640fc2a9092c32a"},
+ {file = "aiohttp-3.13.3-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:5e1d8c8b8f1d91cd08d8f4a3c2b067bfca6ec043d3ff36de0f3a715feeedf926"},
+ {file = "aiohttp-3.13.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:90455115e5da1c3c51ab619ac57f877da8fd6d73c05aacd125c5ae9819582aba"},
+ {file = "aiohttp-3.13.3-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:042e9e0bcb5fba81886c8b4fbb9a09d6b8a00245fd8d88e4d989c1f96c74164c"},
+ {file = "aiohttp-3.13.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2eb752b102b12a76ca02dff751a801f028b4ffbbc478840b473597fc91a9ed43"},
+ {file = "aiohttp-3.13.3-cp314-cp314-win32.whl", hash = "sha256:b556c85915d8efaed322bf1bdae9486aa0f3f764195a0fb6ee962e5c71ef5ce1"},
+ {file = "aiohttp-3.13.3-cp314-cp314-win_amd64.whl", hash = "sha256:9bf9f7a65e7aa20dd764151fb3d616c81088f91f8df39c3893a536e279b4b984"},
+ {file = "aiohttp-3.13.3-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:05861afbbec40650d8a07ea324367cb93e9e8cc7762e04dd4405df99fa65159c"},
+ {file = "aiohttp-3.13.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:2fc82186fadc4a8316768d61f3722c230e2c1dcab4200d52d2ebdf2482e47592"},
+ {file = "aiohttp-3.13.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:0add0900ff220d1d5c5ebbf99ed88b0c1bbf87aa7e4262300ed1376a6b13414f"},
+ {file = "aiohttp-3.13.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:568f416a4072fbfae453dcf9a99194bbb8bdeab718e08ee13dfa2ba0e4bebf29"},
+ {file = "aiohttp-3.13.3-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:add1da70de90a2569c5e15249ff76a631ccacfe198375eead4aadf3b8dc849dc"},
+ {file = "aiohttp-3.13.3-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:10b47b7ba335d2e9b1239fa571131a87e2d8ec96b333e68b2a305e7a98b0bae2"},
+ {file = "aiohttp-3.13.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3dd4dce1c718e38081c8f35f323209d4c1df7d4db4bab1b5c88a6b4d12b74587"},
+ {file = "aiohttp-3.13.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:34bac00a67a812570d4a460447e1e9e06fae622946955f939051e7cc895cfab8"},
+ {file = "aiohttp-3.13.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a19884d2ee70b06d9204b2727a7b9f983d0c684c650254679e716b0b77920632"},
+ {file = "aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5f8ca7f2bb6ba8348a3614c7918cc4bb73268c5ac2a207576b7afea19d3d9f64"},
+ {file = "aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:b0d95340658b9d2f11d9697f59b3814a9d3bb4b7a7c20b131df4bcef464037c0"},
+ {file = "aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:a1e53262fd202e4b40b70c3aff944a8155059beedc8a89bba9dc1f9ef06a1b56"},
+ {file = "aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:d60ac9663f44168038586cab2157e122e46bdef09e9368b37f2d82d354c23f72"},
+ {file = "aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:90751b8eed69435bac9ff4e3d2f6b3af1f57e37ecb0fbeee59c0174c9e2d41df"},
+ {file = "aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:fc353029f176fd2b3ec6cfc71be166aba1936fe5d73dd1992ce289ca6647a9aa"},
+ {file = "aiohttp-3.13.3-cp314-cp314t-win32.whl", hash = "sha256:2e41b18a58da1e474a057b3d35248d8320029f61d70a37629535b16a0c8f3767"},
+ {file = "aiohttp-3.13.3-cp314-cp314t-win_amd64.whl", hash = "sha256:44531a36aa2264a1860089ffd4dce7baf875ee5a6079d5fb42e261c704ef7344"},
+ {file = "aiohttp-3.13.3-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:31a83ea4aead760dfcb6962efb1d861db48c34379f2ff72db9ddddd4cda9ea2e"},
+ {file = "aiohttp-3.13.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:988a8c5e317544fdf0d39871559e67b6341065b87fceac641108c2096d5506b7"},
+ {file = "aiohttp-3.13.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:9b174f267b5cfb9a7dba9ee6859cecd234e9a681841eb85068059bc867fb8f02"},
+ {file = "aiohttp-3.13.3-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:947c26539750deeaee933b000fb6517cc770bbd064bad6033f1cff4803881e43"},
+ {file = "aiohttp-3.13.3-cp39-cp39-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:9ebf57d09e131f5323464bd347135a88622d1c0976e88ce15b670e7ad57e4bd6"},
+ {file = "aiohttp-3.13.3-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4ae5b5a0e1926e504c81c5b84353e7a5516d8778fbbff00429fe7b05bb25cbce"},
+ {file = "aiohttp-3.13.3-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2ba0eea45eb5cc3172dbfc497c066f19c41bac70963ea1a67d51fc92e4cf9a80"},
+ {file = "aiohttp-3.13.3-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bae5c2ed2eae26cc382020edad80d01f36cb8e746da40b292e68fec40421dc6a"},
+ {file = "aiohttp-3.13.3-cp39-cp39-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8a60e60746623925eab7d25823329941aee7242d559baa119ca2b253c88a7bd6"},
+ {file = "aiohttp-3.13.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:e50a2e1404f063427c9d027378472316201a2290959a295169bcf25992d04558"},
+ {file = "aiohttp-3.13.3-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:9a9dc347e5a3dc7dfdbc1f82da0ef29e388ddb2ed281bfce9dd8248a313e62b7"},
+ {file = "aiohttp-3.13.3-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:b46020d11d23fe16551466c77823df9cc2f2c1e63cc965daf67fa5eec6ca1877"},
+ {file = "aiohttp-3.13.3-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:69c56fbc1993fa17043e24a546959c0178fe2b5782405ad4559e6c13975c15e3"},
+ {file = "aiohttp-3.13.3-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:b99281b0704c103d4e11e72a76f1b543d4946fea7dd10767e7e1b5f00d4e5704"},
+ {file = "aiohttp-3.13.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:40c5e40ecc29ba010656c18052b877a1c28f84344825efa106705e835c28530f"},
+ {file = "aiohttp-3.13.3-cp39-cp39-win32.whl", hash = "sha256:56339a36b9f1fc708260c76c87e593e2afb30d26de9ae1eb445b5e051b98a7a1"},
+ {file = "aiohttp-3.13.3-cp39-cp39-win_amd64.whl", hash = "sha256:c6b8568a3bb5819a0ad087f16d40e5a3fb6099f39ea1d5625a3edc1e923fc538"},
+ {file = "aiohttp-3.13.3.tar.gz", hash = "sha256:a949eee43d3782f2daae4f4a2819b2cb9b0c5d3b7f7a927067cc84dafdbb9f88"},
]
[package.dependencies]
@@ -143,7 +177,7 @@ propcache = ">=0.2.0"
yarl = ">=1.17.0,<2.0"
[package.extras]
-speedups = ["Brotli ; platform_python_implementation == \"CPython\"", "aiodns (>=3.3.0)", "brotlicffi ; platform_python_implementation != \"CPython\""]
+speedups = ["Brotli (>=1.2) ; platform_python_implementation == \"CPython\"", "aiodns (>=3.3.0)", "backports.zstd ; platform_python_implementation == \"CPython\" and python_version < \"3.14\"", "brotlicffi (>=1.2) ; platform_python_implementation != \"CPython\""]
[[package]]
name = "aiosignal"
@@ -1545,84 +1579,101 @@ files = [
[[package]]
name = "cffi"
-version = "1.17.1"
+version = "2.0.0"
description = "Foreign Function Interface for Python calling C code."
optional = false
-python-versions = ">=3.8"
+python-versions = ">=3.9"
groups = ["main", "dev"]
+markers = "platform_python_implementation != \"PyPy\""
files = [
- {file = "cffi-1.17.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:df8b1c11f177bc2313ec4b2d46baec87a5f3e71fc8b45dab2ee7cae86d9aba14"},
- {file = "cffi-1.17.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8f2cdc858323644ab277e9bb925ad72ae0e67f69e804f4898c070998d50b1a67"},
- {file = "cffi-1.17.1-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:edae79245293e15384b51f88b00613ba9f7198016a5948b5dddf4917d4d26382"},
- {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:45398b671ac6d70e67da8e4224a065cec6a93541bb7aebe1b198a61b58c7b702"},
- {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ad9413ccdeda48c5afdae7e4fa2192157e991ff761e7ab8fdd8926f40b160cc3"},
- {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5da5719280082ac6bd9aa7becb3938dc9f9cbd57fac7d2871717b1feb0902ab6"},
- {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2bb1a08b8008b281856e5971307cc386a8e9c5b625ac297e853d36da6efe9c17"},
- {file = "cffi-1.17.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:045d61c734659cc045141be4bae381a41d89b741f795af1dd018bfb532fd0df8"},
- {file = "cffi-1.17.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:6883e737d7d9e4899a8a695e00ec36bd4e5e4f18fabe0aca0efe0a4b44cdb13e"},
- {file = "cffi-1.17.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:6b8b4a92e1c65048ff98cfe1f735ef8f1ceb72e3d5f0c25fdb12087a23da22be"},
- {file = "cffi-1.17.1-cp310-cp310-win32.whl", hash = "sha256:c9c3d058ebabb74db66e431095118094d06abf53284d9c81f27300d0e0d8bc7c"},
- {file = "cffi-1.17.1-cp310-cp310-win_amd64.whl", hash = "sha256:0f048dcf80db46f0098ccac01132761580d28e28bc0f78ae0d58048063317e15"},
- {file = "cffi-1.17.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a45e3c6913c5b87b3ff120dcdc03f6131fa0065027d0ed7ee6190736a74cd401"},
- {file = "cffi-1.17.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:30c5e0cb5ae493c04c8b42916e52ca38079f1b235c2f8ae5f4527b963c401caf"},
- {file = "cffi-1.17.1-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f75c7ab1f9e4aca5414ed4d8e5c0e303a34f4421f8a0d47a4d019ceff0ab6af4"},
- {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a1ed2dd2972641495a3ec98445e09766f077aee98a1c896dcb4ad0d303628e41"},
- {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:46bf43160c1a35f7ec506d254e5c890f3c03648a4dbac12d624e4490a7046cd1"},
- {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a24ed04c8ffd54b0729c07cee15a81d964e6fee0e3d4d342a27b020d22959dc6"},
- {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:610faea79c43e44c71e1ec53a554553fa22321b65fae24889706c0a84d4ad86d"},
- {file = "cffi-1.17.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:a9b15d491f3ad5d692e11f6b71f7857e7835eb677955c00cc0aefcd0669adaf6"},
- {file = "cffi-1.17.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:de2ea4b5833625383e464549fec1bc395c1bdeeb5f25c4a3a82b5a8c756ec22f"},
- {file = "cffi-1.17.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:fc48c783f9c87e60831201f2cce7f3b2e4846bf4d8728eabe54d60700b318a0b"},
- {file = "cffi-1.17.1-cp311-cp311-win32.whl", hash = "sha256:85a950a4ac9c359340d5963966e3e0a94a676bd6245a4b55bc43949eee26a655"},
- {file = "cffi-1.17.1-cp311-cp311-win_amd64.whl", hash = "sha256:caaf0640ef5f5517f49bc275eca1406b0ffa6aa184892812030f04c2abf589a0"},
- {file = "cffi-1.17.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:805b4371bf7197c329fcb3ead37e710d1bca9da5d583f5073b799d5c5bd1eee4"},
- {file = "cffi-1.17.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:733e99bc2df47476e3848417c5a4540522f234dfd4ef3ab7fafdf555b082ec0c"},
- {file = "cffi-1.17.1-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1257bdabf294dceb59f5e70c64a3e2f462c30c7ad68092d01bbbfb1c16b1ba36"},
- {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da95af8214998d77a98cc14e3a3bd00aa191526343078b530ceb0bd710fb48a5"},
- {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d63afe322132c194cf832bfec0dc69a99fb9bb6bbd550f161a49e9e855cc78ff"},
- {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f79fc4fc25f1c8698ff97788206bb3c2598949bfe0fef03d299eb1b5356ada99"},
- {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b62ce867176a75d03a665bad002af8e6d54644fad99a3c70905c543130e39d93"},
- {file = "cffi-1.17.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:386c8bf53c502fff58903061338ce4f4950cbdcb23e2902d86c0f722b786bbe3"},
- {file = "cffi-1.17.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4ceb10419a9adf4460ea14cfd6bc43d08701f0835e979bf821052f1805850fe8"},
- {file = "cffi-1.17.1-cp312-cp312-win32.whl", hash = "sha256:a08d7e755f8ed21095a310a693525137cfe756ce62d066e53f502a83dc550f65"},
- {file = "cffi-1.17.1-cp312-cp312-win_amd64.whl", hash = "sha256:51392eae71afec0d0c8fb1a53b204dbb3bcabcb3c9b807eedf3e1e6ccf2de903"},
- {file = "cffi-1.17.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f3a2b4222ce6b60e2e8b337bb9596923045681d71e5a082783484d845390938e"},
- {file = "cffi-1.17.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0984a4925a435b1da406122d4d7968dd861c1385afe3b45ba82b750f229811e2"},
- {file = "cffi-1.17.1-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d01b12eeeb4427d3110de311e1774046ad344f5b1a7403101878976ecd7a10f3"},
- {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:706510fe141c86a69c8ddc029c7910003a17353970cff3b904ff0686a5927683"},
- {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de55b766c7aa2e2a3092c51e0483d700341182f08e67c63630d5b6f200bb28e5"},
- {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c59d6e989d07460165cc5ad3c61f9fd8f1b4796eacbd81cee78957842b834af4"},
- {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd398dbc6773384a17fe0d3e7eeb8d1a21c2200473ee6806bb5e6a8e62bb73dd"},
- {file = "cffi-1.17.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:3edc8d958eb099c634dace3c7e16560ae474aa3803a5df240542b305d14e14ed"},
- {file = "cffi-1.17.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:72e72408cad3d5419375fc87d289076ee319835bdfa2caad331e377589aebba9"},
- {file = "cffi-1.17.1-cp313-cp313-win32.whl", hash = "sha256:e03eab0a8677fa80d646b5ddece1cbeaf556c313dcfac435ba11f107ba117b5d"},
- {file = "cffi-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:f6a16c31041f09ead72d69f583767292f750d24913dadacf5756b966aacb3f1a"},
- {file = "cffi-1.17.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:636062ea65bd0195bc012fea9321aca499c0504409f413dc88af450b57ffd03b"},
- {file = "cffi-1.17.1-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c7eac2ef9b63c79431bc4b25f1cd649d7f061a28808cbc6c47b534bd789ef964"},
- {file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e221cf152cff04059d011ee126477f0d9588303eb57e88923578ace7baad17f9"},
- {file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:31000ec67d4221a71bd3f67df918b1f88f676f1c3b535a7eb473255fdc0b83fc"},
- {file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6f17be4345073b0a7b8ea599688f692ac3ef23ce28e5df79c04de519dbc4912c"},
- {file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0e2b1fac190ae3ebfe37b979cc1ce69c81f4e4fe5746bb401dca63a9062cdaf1"},
- {file = "cffi-1.17.1-cp38-cp38-win32.whl", hash = "sha256:7596d6620d3fa590f677e9ee430df2958d2d6d6de2feeae5b20e82c00b76fbf8"},
- {file = "cffi-1.17.1-cp38-cp38-win_amd64.whl", hash = "sha256:78122be759c3f8a014ce010908ae03364d00a1f81ab5c7f4a7a5120607ea56e1"},
- {file = "cffi-1.17.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b2ab587605f4ba0bf81dc0cb08a41bd1c0a5906bd59243d56bad7668a6fc6c16"},
- {file = "cffi-1.17.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:28b16024becceed8c6dfbc75629e27788d8a3f9030691a1dbf9821a128b22c36"},
- {file = "cffi-1.17.1-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1d599671f396c4723d016dbddb72fe8e0397082b0a77a4fab8028923bec050e8"},
- {file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca74b8dbe6e8e8263c0ffd60277de77dcee6c837a3d0881d8c1ead7268c9e576"},
- {file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f7f5baafcc48261359e14bcd6d9bff6d4b28d9103847c9e136694cb0501aef87"},
- {file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:98e3969bcff97cae1b2def8ba499ea3d6f31ddfdb7635374834cf89a1a08ecf0"},
- {file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cdf5ce3acdfd1661132f2a9c19cac174758dc2352bfe37d98aa7512c6b7178b3"},
- {file = "cffi-1.17.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:9755e4345d1ec879e3849e62222a18c7174d65a6a92d5b346b1863912168b595"},
- {file = "cffi-1.17.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:f1e22e8c4419538cb197e4dd60acc919d7696e5ef98ee4da4e01d3f8cfa4cc5a"},
- {file = "cffi-1.17.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:c03e868a0b3bc35839ba98e74211ed2b05d2119be4e8a0f224fba9384f1fe02e"},
- {file = "cffi-1.17.1-cp39-cp39-win32.whl", hash = "sha256:e31ae45bc2e29f6b2abd0de1cc3b9d5205aa847cafaecb8af1476a609a2f6eb7"},
- {file = "cffi-1.17.1-cp39-cp39-win_amd64.whl", hash = "sha256:d016c76bdd850f3c626af19b0542c9677ba156e4ee4fccfdd7848803533ef662"},
- {file = "cffi-1.17.1.tar.gz", hash = "sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824"},
+ {file = "cffi-2.0.0-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:0cf2d91ecc3fcc0625c2c530fe004f82c110405f101548512cce44322fa8ac44"},
+ {file = "cffi-2.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f73b96c41e3b2adedc34a7356e64c8eb96e03a3782b535e043a986276ce12a49"},
+ {file = "cffi-2.0.0-cp310-cp310-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:53f77cbe57044e88bbd5ed26ac1d0514d2acf0591dd6bb02a3ae37f76811b80c"},
+ {file = "cffi-2.0.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3e837e369566884707ddaf85fc1744b47575005c0a229de3327f8f9a20f4efeb"},
+ {file = "cffi-2.0.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:5eda85d6d1879e692d546a078b44251cdd08dd1cfb98dfb77b670c97cee49ea0"},
+ {file = "cffi-2.0.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9332088d75dc3241c702d852d4671613136d90fa6881da7d770a483fd05248b4"},
+ {file = "cffi-2.0.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fc7de24befaeae77ba923797c7c87834c73648a05a4bde34b3b7e5588973a453"},
+ {file = "cffi-2.0.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:cf364028c016c03078a23b503f02058f1814320a56ad535686f90565636a9495"},
+ {file = "cffi-2.0.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e11e82b744887154b182fd3e7e8512418446501191994dbf9c9fc1f32cc8efd5"},
+ {file = "cffi-2.0.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8ea985900c5c95ce9db1745f7933eeef5d314f0565b27625d9a10ec9881e1bfb"},
+ {file = "cffi-2.0.0-cp310-cp310-win32.whl", hash = "sha256:1f72fb8906754ac8a2cc3f9f5aaa298070652a0ffae577e0ea9bd480dc3c931a"},
+ {file = "cffi-2.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:b18a3ed7d5b3bd8d9ef7a8cb226502c6bf8308df1525e1cc676c3680e7176739"},
+ {file = "cffi-2.0.0-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:b4c854ef3adc177950a8dfc81a86f5115d2abd545751a304c5bcf2c2c7283cfe"},
+ {file = "cffi-2.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2de9a304e27f7596cd03d16f1b7c72219bd944e99cc52b84d0145aefb07cbd3c"},
+ {file = "cffi-2.0.0-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:baf5215e0ab74c16e2dd324e8ec067ef59e41125d3eade2b863d294fd5035c92"},
+ {file = "cffi-2.0.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:730cacb21e1bdff3ce90babf007d0a0917cc3e6492f336c2f0134101e0944f93"},
+ {file = "cffi-2.0.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:6824f87845e3396029f3820c206e459ccc91760e8fa24422f8b0c3d1731cbec5"},
+ {file = "cffi-2.0.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9de40a7b0323d889cf8d23d1ef214f565ab154443c42737dfe52ff82cf857664"},
+ {file = "cffi-2.0.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8941aaadaf67246224cee8c3803777eed332a19d909b47e29c9842ef1e79ac26"},
+ {file = "cffi-2.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a05d0c237b3349096d3981b727493e22147f934b20f6f125a3eba8f994bec4a9"},
+ {file = "cffi-2.0.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:94698a9c5f91f9d138526b48fe26a199609544591f859c870d477351dc7b2414"},
+ {file = "cffi-2.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:5fed36fccc0612a53f1d4d9a816b50a36702c28a2aa880cb8a122b3466638743"},
+ {file = "cffi-2.0.0-cp311-cp311-win32.whl", hash = "sha256:c649e3a33450ec82378822b3dad03cc228b8f5963c0c12fc3b1e0ab940f768a5"},
+ {file = "cffi-2.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:66f011380d0e49ed280c789fbd08ff0d40968ee7b665575489afa95c98196ab5"},
+ {file = "cffi-2.0.0-cp311-cp311-win_arm64.whl", hash = "sha256:c6638687455baf640e37344fe26d37c404db8b80d037c3d29f58fe8d1c3b194d"},
+ {file = "cffi-2.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d02d6655b0e54f54c4ef0b94eb6be0607b70853c45ce98bd278dc7de718be5d"},
+ {file = "cffi-2.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8eca2a813c1cb7ad4fb74d368c2ffbbb4789d377ee5bb8df98373c2cc0dee76c"},
+ {file = "cffi-2.0.0-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:21d1152871b019407d8ac3985f6775c079416c282e431a4da6afe7aefd2bccbe"},
+ {file = "cffi-2.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b21e08af67b8a103c71a250401c78d5e0893beff75e28c53c98f4de42f774062"},
+ {file = "cffi-2.0.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:1e3a615586f05fc4065a8b22b8152f0c1b00cdbc60596d187c2a74f9e3036e4e"},
+ {file = "cffi-2.0.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:81afed14892743bbe14dacb9e36d9e0e504cd204e0b165062c488942b9718037"},
+ {file = "cffi-2.0.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3e17ed538242334bf70832644a32a7aae3d83b57567f9fd60a26257e992b79ba"},
+ {file = "cffi-2.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3925dd22fa2b7699ed2617149842d2e6adde22b262fcbfada50e3d195e4b3a94"},
+ {file = "cffi-2.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2c8f814d84194c9ea681642fd164267891702542f028a15fc97d4674b6206187"},
+ {file = "cffi-2.0.0-cp312-cp312-win32.whl", hash = "sha256:da902562c3e9c550df360bfa53c035b2f241fed6d9aef119048073680ace4a18"},
+ {file = "cffi-2.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:da68248800ad6320861f129cd9c1bf96ca849a2771a59e0344e88681905916f5"},
+ {file = "cffi-2.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:4671d9dd5ec934cb9a73e7ee9676f9362aba54f7f34910956b84d727b0d73fb6"},
+ {file = "cffi-2.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:00bdf7acc5f795150faa6957054fbbca2439db2f775ce831222b66f192f03beb"},
+ {file = "cffi-2.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:45d5e886156860dc35862657e1494b9bae8dfa63bf56796f2fb56e1679fc0bca"},
+ {file = "cffi-2.0.0-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:07b271772c100085dd28b74fa0cd81c8fb1a3ba18b21e03d7c27f3436a10606b"},
+ {file = "cffi-2.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d48a880098c96020b02d5a1f7d9251308510ce8858940e6fa99ece33f610838b"},
+ {file = "cffi-2.0.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f93fd8e5c8c0a4aa1f424d6173f14a892044054871c771f8566e4008eaa359d2"},
+ {file = "cffi-2.0.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:dd4f05f54a52fb558f1ba9f528228066954fee3ebe629fc1660d874d040ae5a3"},
+ {file = "cffi-2.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c8d3b5532fc71b7a77c09192b4a5a200ea992702734a2e9279a37f2478236f26"},
+ {file = "cffi-2.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d9b29c1f0ae438d5ee9acb31cadee00a58c46cc9c0b2f9038c6b0b3470877a8c"},
+ {file = "cffi-2.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6d50360be4546678fc1b79ffe7a66265e28667840010348dd69a314145807a1b"},
+ {file = "cffi-2.0.0-cp313-cp313-win32.whl", hash = "sha256:74a03b9698e198d47562765773b4a8309919089150a0bb17d829ad7b44b60d27"},
+ {file = "cffi-2.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:19f705ada2530c1167abacb171925dd886168931e0a7b78f5bffcae5c6b5be75"},
+ {file = "cffi-2.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:256f80b80ca3853f90c21b23ee78cd008713787b1b1e93eae9f3d6a7134abd91"},
+ {file = "cffi-2.0.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fc33c5141b55ed366cfaad382df24fe7dcbc686de5be719b207bb248e3053dc5"},
+ {file = "cffi-2.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c654de545946e0db659b3400168c9ad31b5d29593291482c43e3564effbcee13"},
+ {file = "cffi-2.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:24b6f81f1983e6df8db3adc38562c83f7d4a0c36162885ec7f7b77c7dcbec97b"},
+ {file = "cffi-2.0.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:12873ca6cb9b0f0d3a0da705d6086fe911591737a59f28b7936bdfed27c0d47c"},
+ {file = "cffi-2.0.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:d9b97165e8aed9272a6bb17c01e3cc5871a594a446ebedc996e2397a1c1ea8ef"},
+ {file = "cffi-2.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:afb8db5439b81cf9c9d0c80404b60c3cc9c3add93e114dcae767f1477cb53775"},
+ {file = "cffi-2.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:737fe7d37e1a1bffe70bd5754ea763a62a066dc5913ca57e957824b72a85e205"},
+ {file = "cffi-2.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:38100abb9d1b1435bc4cc340bb4489635dc2f0da7456590877030c9b3d40b0c1"},
+ {file = "cffi-2.0.0-cp314-cp314-win32.whl", hash = "sha256:087067fa8953339c723661eda6b54bc98c5625757ea62e95eb4898ad5e776e9f"},
+ {file = "cffi-2.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:203a48d1fb583fc7d78a4c6655692963b860a417c0528492a6bc21f1aaefab25"},
+ {file = "cffi-2.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:dbd5c7a25a7cb98f5ca55d258b103a2054f859a46ae11aaf23134f9cc0d356ad"},
+ {file = "cffi-2.0.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:9a67fc9e8eb39039280526379fb3a70023d77caec1852002b4da7e8b270c4dd9"},
+ {file = "cffi-2.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7a66c7204d8869299919db4d5069a82f1561581af12b11b3c9f48c584eb8743d"},
+ {file = "cffi-2.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7cc09976e8b56f8cebd752f7113ad07752461f48a58cbba644139015ac24954c"},
+ {file = "cffi-2.0.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:92b68146a71df78564e4ef48af17551a5ddd142e5190cdf2c5624d0c3ff5b2e8"},
+ {file = "cffi-2.0.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b1e74d11748e7e98e2f426ab176d4ed720a64412b6a15054378afdb71e0f37dc"},
+ {file = "cffi-2.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:28a3a209b96630bca57cce802da70c266eb08c6e97e5afd61a75611ee6c64592"},
+ {file = "cffi-2.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7553fb2090d71822f02c629afe6042c299edf91ba1bf94951165613553984512"},
+ {file = "cffi-2.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6c6c373cfc5c83a975506110d17457138c8c63016b563cc9ed6e056a82f13ce4"},
+ {file = "cffi-2.0.0-cp314-cp314t-win32.whl", hash = "sha256:1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e"},
+ {file = "cffi-2.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6"},
+ {file = "cffi-2.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9"},
+ {file = "cffi-2.0.0-cp39-cp39-macosx_10_13_x86_64.whl", hash = "sha256:fe562eb1a64e67dd297ccc4f5addea2501664954f2692b69a76449ec7913ecbf"},
+ {file = "cffi-2.0.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:de8dad4425a6ca6e4e5e297b27b5c824ecc7581910bf9aee86cb6835e6812aa7"},
+ {file = "cffi-2.0.0-cp39-cp39-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:4647afc2f90d1ddd33441e5b0e85b16b12ddec4fca55f0d9671fef036ecca27c"},
+ {file = "cffi-2.0.0-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3f4d46d8b35698056ec29bca21546e1551a205058ae1a181d871e278b0b28165"},
+ {file = "cffi-2.0.0-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:e6e73b9e02893c764e7e8d5bb5ce277f1a009cd5243f8228f75f842bf937c534"},
+ {file = "cffi-2.0.0-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:cb527a79772e5ef98fb1d700678fe031e353e765d1ca2d409c92263c6d43e09f"},
+ {file = "cffi-2.0.0-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:61d028e90346df14fedc3d1e5441df818d095f3b87d286825dfcbd6459b7ef63"},
+ {file = "cffi-2.0.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:0f6084a0ea23d05d20c3edcda20c3d006f9b6f3fefeac38f59262e10cef47ee2"},
+ {file = "cffi-2.0.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:1cd13c99ce269b3ed80b417dcd591415d3372bcac067009b6e0f59c7d4015e65"},
+ {file = "cffi-2.0.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:89472c9762729b5ae1ad974b777416bfda4ac5642423fa93bd57a09204712322"},
+ {file = "cffi-2.0.0-cp39-cp39-win32.whl", hash = "sha256:2081580ebb843f759b9f617314a24ed5738c51d2aee65d31e02f6f7a2b97707a"},
+ {file = "cffi-2.0.0-cp39-cp39-win_amd64.whl", hash = "sha256:b882b3df248017dba09d6b16defe9b5c407fe32fc7c65a9c69798e6175601be9"},
+ {file = "cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529"},
]
-markers = {dev = "platform_python_implementation != \"PyPy\""}
[package.dependencies]
-pycparser = "*"
+pycparser = {version = "*", markers = "implementation_name != \"PyPy\""}
[[package]]
name = "cfgv"
@@ -3238,14 +3289,14 @@ files = [
[[package]]
name = "marshmallow"
-version = "3.26.1"
+version = "3.26.2"
description = "A lightweight library for converting complex datatypes to and from native Python datatypes."
optional = false
python-versions = ">=3.9"
groups = ["dev"]
files = [
- {file = "marshmallow-3.26.1-py3-none-any.whl", hash = "sha256:3350409f20a70a7e4e11a27661187b77cdcaeb20abca41c1454fe33636bea09c"},
- {file = "marshmallow-3.26.1.tar.gz", hash = "sha256:e6d8affb6cb61d39d26402096dc0aee12d5a26d490a121f118d2e81dc0719dc6"},
+ {file = "marshmallow-3.26.2-py3-none-any.whl", hash = "sha256:013fa8a3c4c276c24d26d84ce934dc964e2aa794345a0f8c7e5a7191482c8a73"},
+ {file = "marshmallow-3.26.2.tar.gz", hash = "sha256:bbe2adb5a03e6e3571b573f42527c6fe926e17467833660bebd11593ab8dfd57"},
]
[package.dependencies]
@@ -4529,11 +4580,11 @@ description = "C parser in Python"
optional = false
python-versions = ">=3.8"
groups = ["main", "dev"]
+markers = "platform_python_implementation != \"PyPy\" and implementation_name != \"PyPy\""
files = [
{file = "pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc"},
{file = "pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6"},
]
-markers = {dev = "platform_python_implementation != \"PyPy\""}
[[package]]
name = "pydantic"
@@ -4770,30 +4821,45 @@ testutils = ["gitpython (>3)"]
[[package]]
name = "pynacl"
-version = "1.5.0"
+version = "1.6.2"
description = "Python binding to the Networking and Cryptography (NaCl) library"
optional = false
-python-versions = ">=3.6"
+python-versions = ">=3.8"
groups = ["main"]
files = [
- {file = "PyNaCl-1.5.0-cp36-abi3-macosx_10_10_universal2.whl", hash = "sha256:401002a4aaa07c9414132aaed7f6836ff98f59277a234704ff66878c2ee4a0d1"},
- {file = "PyNaCl-1.5.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:52cb72a79269189d4e0dc537556f4740f7f0a9ec41c1322598799b0bdad4ef92"},
- {file = "PyNaCl-1.5.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a36d4a9dda1f19ce6e03c9a784a2921a4b726b02e1c736600ca9c22029474394"},
- {file = "PyNaCl-1.5.0-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:0c84947a22519e013607c9be43706dd42513f9e6ae5d39d3613ca1e142fba44d"},
- {file = "PyNaCl-1.5.0-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:06b8f6fa7f5de8d5d2f7573fe8c863c051225a27b61e6860fd047b1775807858"},
- {file = "PyNaCl-1.5.0-cp36-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:a422368fc821589c228f4c49438a368831cb5bbc0eab5ebe1d7fac9dded6567b"},
- {file = "PyNaCl-1.5.0-cp36-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:61f642bf2378713e2c2e1de73444a3778e5f0a38be6fee0fe532fe30060282ff"},
- {file = "PyNaCl-1.5.0-cp36-abi3-win32.whl", hash = "sha256:e46dae94e34b085175f8abb3b0aaa7da40767865ac82c928eeb9e57e1ea8a543"},
- {file = "PyNaCl-1.5.0-cp36-abi3-win_amd64.whl", hash = "sha256:20f42270d27e1b6a29f54032090b972d97f0a1b0948cc52392041ef7831fee93"},
- {file = "PyNaCl-1.5.0.tar.gz", hash = "sha256:8ac7448f09ab85811607bdd21ec2464495ac8b7c66d146bf545b0f08fb9220ba"},
+ {file = "pynacl-1.6.2-cp314-cp314t-macosx_10_10_universal2.whl", hash = "sha256:622d7b07cc5c02c666795792931b50c91f3ce3c2649762efb1ef0d5684c81594"},
+ {file = "pynacl-1.6.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d071c6a9a4c94d79eb665db4ce5cedc537faf74f2355e4d502591d850d3913c0"},
+ {file = "pynacl-1.6.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fe9847ca47d287af41e82be1dd5e23023d3c31a951da134121ab02e42ac218c9"},
+ {file = "pynacl-1.6.2-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:04316d1fc625d860b6c162fff704eb8426b1a8bcd3abacea11142cbd99a6b574"},
+ {file = "pynacl-1.6.2-cp314-cp314t-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:44081faff368d6c5553ccf55322ef2819abb40e25afaec7e740f159f74813634"},
+ {file = "pynacl-1.6.2-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:a9f9932d8d2811ce1a8ffa79dcbdf3970e7355b5c8eb0c1a881a57e7f7d96e88"},
+ {file = "pynacl-1.6.2-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:bc4a36b28dd72fb4845e5d8f9760610588a96d5a51f01d84d8c6ff9849968c14"},
+ {file = "pynacl-1.6.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:3bffb6d0f6becacb6526f8f42adfb5efb26337056ee0831fb9a7044d1a964444"},
+ {file = "pynacl-1.6.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:2fef529ef3ee487ad8113d287a593fa26f48ee3620d92ecc6f1d09ea38e0709b"},
+ {file = "pynacl-1.6.2-cp314-cp314t-win32.whl", hash = "sha256:a84bf1c20339d06dc0c85d9aea9637a24f718f375d861b2668b2f9f96fa51145"},
+ {file = "pynacl-1.6.2-cp314-cp314t-win_amd64.whl", hash = "sha256:320ef68a41c87547c91a8b58903c9caa641ab01e8512ce291085b5fe2fcb7590"},
+ {file = "pynacl-1.6.2-cp314-cp314t-win_arm64.whl", hash = "sha256:d29bfe37e20e015a7d8b23cfc8bd6aa7909c92a1b8f41ee416bbb3e79ef182b2"},
+ {file = "pynacl-1.6.2-cp38-abi3-macosx_10_10_universal2.whl", hash = "sha256:c949ea47e4206af7c8f604b8278093b674f7c79ed0d4719cc836902bf4517465"},
+ {file = "pynacl-1.6.2-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8845c0631c0be43abdd865511c41eab235e0be69c81dc66a50911594198679b0"},
+ {file = "pynacl-1.6.2-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:22de65bb9010a725b0dac248f353bb072969c94fa8d6b1f34b87d7953cf7bbe4"},
+ {file = "pynacl-1.6.2-cp38-abi3-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:46065496ab748469cdd999246d17e301b2c24ae2fdf739132e580a0e94c94a87"},
+ {file = "pynacl-1.6.2-cp38-abi3-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8a66d6fb6ae7661c58995f9c6435bda2b1e68b54b598a6a10247bfcdadac996c"},
+ {file = "pynacl-1.6.2-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:26bfcd00dcf2cf160f122186af731ae30ab120c18e8375684ec2670dccd28130"},
+ {file = "pynacl-1.6.2-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:c8a231e36ec2cab018c4ad4358c386e36eede0319a0c41fed24f840b1dac59f6"},
+ {file = "pynacl-1.6.2-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:68be3a09455743ff9505491220b64440ced8973fe930f270c8e07ccfa25b1f9e"},
+ {file = "pynacl-1.6.2-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:8b097553b380236d51ed11356c953bf8ce36a29a3e596e934ecabe76c985a577"},
+ {file = "pynacl-1.6.2-cp38-abi3-win32.whl", hash = "sha256:5811c72b473b2f38f7e2a3dc4f8642e3a3e9b5e7317266e4ced1fba85cae41aa"},
+ {file = "pynacl-1.6.2-cp38-abi3-win_amd64.whl", hash = "sha256:62985f233210dee6548c223301b6c25440852e13d59a8b81490203c3227c5ba0"},
+ {file = "pynacl-1.6.2-cp38-abi3-win_arm64.whl", hash = "sha256:834a43af110f743a754448463e8fd61259cd4ab5bbedcf70f9dabad1d28a394c"},
+ {file = "pynacl-1.6.2.tar.gz", hash = "sha256:018494d6d696ae03c7e656e5e74cdfd8ea1326962cc401bcf018f1ed8436811c"},
]
[package.dependencies]
-cffi = ">=1.4.1"
+cffi = {version = ">=2.0.0", markers = "platform_python_implementation != \"PyPy\" and python_version >= \"3.9\""}
[package.extras]
-docs = ["sphinx (>=1.6.5)", "sphinx-rtd-theme"]
-tests = ["hypothesis (>=3.27.0)", "pytest (>=3.2.1,!=3.3.0)"]
+docs = ["sphinx (<7)", "sphinx_rtd_theme"]
+tests = ["hypothesis (>=3.27.0)", "pytest (>=7.4.0)", "pytest-cov (>=2.10.1)", "pytest-xdist (>=3.5.0)"]
[[package]]
name = "pyopenssl"
@@ -5980,22 +6046,22 @@ socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"]
[[package]]
name = "urllib3"
-version = "2.5.0"
+version = "2.6.3"
description = "HTTP library with thread-safe connection pooling, file post, and more."
optional = false
python-versions = ">=3.9"
groups = ["main", "dev"]
markers = "python_version >= \"3.10\""
files = [
- {file = "urllib3-2.5.0-py3-none-any.whl", hash = "sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc"},
- {file = "urllib3-2.5.0.tar.gz", hash = "sha256:3fc47733c7e419d4bc3f6b3dc2b4f890bb743906a30d56ba4a5bfa4bbff92760"},
+ {file = "urllib3-2.6.3-py3-none-any.whl", hash = "sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4"},
+ {file = "urllib3-2.6.3.tar.gz", hash = "sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed"},
]
[package.extras]
-brotli = ["brotli (>=1.0.9) ; platform_python_implementation == \"CPython\"", "brotlicffi (>=0.8.0) ; platform_python_implementation != \"CPython\""]
+brotli = ["brotli (>=1.2.0) ; platform_python_implementation == \"CPython\"", "brotlicffi (>=1.2.0.0) ; platform_python_implementation != \"CPython\""]
h2 = ["h2 (>=4,<5)"]
socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"]
-zstd = ["zstandard (>=0.18.0)"]
+zstd = ["backports-zstd (>=1.0.0) ; python_version < \"3.14\""]
[[package]]
name = "virtualenv"
@@ -6052,18 +6118,18 @@ test = ["websockets"]
[[package]]
name = "werkzeug"
-version = "3.1.3"
+version = "3.1.5"
description = "The comprehensive WSGI web application library."
optional = false
python-versions = ">=3.9"
groups = ["main", "dev"]
files = [
- {file = "werkzeug-3.1.3-py3-none-any.whl", hash = "sha256:54b78bf3716d19a65be4fceccc0d1d7b89e608834989dfae50ea87564639213e"},
- {file = "werkzeug-3.1.3.tar.gz", hash = "sha256:60723ce945c19328679790e3282cc758aa4a6040e4bb330f53d30fa546d44746"},
+ {file = "werkzeug-3.1.5-py3-none-any.whl", hash = "sha256:5111e36e91086ece91f93268bb39b4a35c1e6f1feac762c9c822ded0a4e322dc"},
+ {file = "werkzeug-3.1.5.tar.gz", hash = "sha256:6a548b0e88955dd07ccb25539d7d0cc97417ee9e179677d22c7041c8f078ce67"},
]
[package.dependencies]
-MarkupSafe = ">=2.1.1"
+markupsafe = ">=2.1.1"
[package.extras]
watchdog = ["watchdog (>=2.3)"]
diff --git a/prowler/AGENTS.md b/prowler/AGENTS.md
index 0a9fb455e8..86c2244edf 100644
--- a/prowler/AGENTS.md
+++ b/prowler/AGENTS.md
@@ -1,366 +1,125 @@
# Prowler SDK Agent Guide
-**Complete guide for AI agents and developers working on the Prowler SDK - the core Python security scanning engine.**
+> **Skills Reference**: For detailed patterns, use these skills:
+> - [`prowler-sdk-check`](../skills/prowler-sdk-check/SKILL.md) - Create new security checks (step-by-step)
+> - [`prowler-provider`](../skills/prowler-provider/SKILL.md) - Add new cloud providers
+> - [`prowler-test-sdk`](../skills/prowler-test-sdk/SKILL.md) - pytest patterns for SDK
+> - [`prowler-compliance`](../skills/prowler-compliance/SKILL.md) - Compliance framework structure
+> - [`pytest`](../skills/pytest/SKILL.md) - Generic pytest patterns
## Project Overview
-The Prowler SDK is the core Python engine that powers Prowler's cloud security assessment capabilities. It provides:
-
-- **Multi-cloud Security Scanning**: AWS, Azure, GCP, Kubernetes, GitHub, M365, Oracle Cloud, MongoDB Atlas, and more
-- **Compliance Frameworks**: 30+ frameworks including CIS, NIST, PCI-DSS, SOC2, GDPR
-- **1000+ Security Checks**: Comprehensive coverage across all supported providers
-- **Multiple Output Formats**: JSON, CSV, HTML, ASFF, OCSF, and compliance-specific formats
-
-## Mission & Scope
-
-- Maintain and enhance the core Prowler SDK functionality with security and stability as top priorities
-- Follow best practices for Python patterns, code style, security, and comprehensive testing
-- To get more information about development guidelines, please refer to the Prowler Developer Guide in `docs/developer-guide/`
+The Prowler SDK is the core Python engine powering cloud security assessments across AWS, Azure, GCP, Kubernetes, GitHub, M365, and more. It includes 1000+ security checks and 30+ compliance frameworks.
---
-## Architecture Rules
+## CRITICAL RULES
-### 1. Provider Architecture Pattern
-
-All Prowler providers MUST follow the established pattern:
+### Provider Architecture
```
prowler/providers/{provider}/
-├── {provider}_provider.py # Main provider class
-├── models.py # Provider-specific models
-├── config.py # Provider configuration
-├── exceptions/ # Provider-specific exceptions
-├── lib/ # Provider libraries (as minimun it should have implemented the next folders: service, arguments, mutelist)
-│ ├── service/ # Provider-specific service class to be inherited by all services of the provider
-│ ├── arguments/ # Provider-specific CLI arguments parser
-│ └── mutelist/ # Provider-specific mutelist functionality
-└── services/ # All provider services to be audited
- └── {service}/ # Individual service
- ├── {service}_service.py # Class to fetch the needed resources from the API and store them to be used by the checks
- ├── {service}_client.py # Python instance of the service class to be used by the checks
- └── {check_name}/ # Individual check folder
- ├── {check_name}.py # Python class to implement the check logic
- └── {check_name}.metadata.json # JSON file to store the check metadata
- └── {check_name_2}/ # Other checks can be added to the same service folder
- ├── {check_name_2}.py
- └── {check_name_2}.metadata.json
- ...
- └── {service_2}/ # Other services can be added to the same provider folder
- ...
+├── {provider}_provider.py # Main provider class
+├── models.py # Provider-specific models
+├── lib/ # service/, arguments/, mutelist/
+└── services/{service}/
+ ├── {service}_service.py # Resource fetcher
+ ├── {service}_client.py # Singleton instance
+ └── {check_name}/ # Individual checks
+ ├── {check_name}.py
+ └── {check_name}.metadata.json
```
-### 2. Check Implementation Standards
-
-Every security check MUST implement:
+### Check Implementation
```python
-from prowler.lib.check.models import Check, CheckReport
-from prowler.providers..services.._client import _client
+from prowler.lib.check.models import Check, CheckReport{Provider}
+from prowler.providers.{provider}.services.{service}.{service}_client import {service}_client
-class check_name(Check):
- """Ensure that meets ."""
- def execute(self) -> list[CheckReport]:
- """Execute the check logic.
-
- Returns:
- A list of reports containing the result of the check.
- """
+class {check_name}(Check):
+ def execute(self) -> list[CheckReport{Provider}]:
findings = []
- # Check implementation here
- for resource in _client.:
- # Security validation logic
- report = CheckReport(metadata=self.metadata(), resource=resource)
- report.status = "PASS" | "FAIL"
+ for resource in {service}_client.{resources}:
+ report = CheckReport{Provider}(metadata=self.metadata(), resource=resource)
+ report.status = "PASS" if resource.is_compliant else "FAIL"
report.status_extended = "Detailed explanation"
- findings.append(report) # Add the report to the list of findings
+ findings.append(report)
return findings
```
-### 3. Compliance Framework Integration
+### Code Style
-All compliance frameworks must be defined in:
-- `prowler/compliance/{provider}/{framework}.json`
-- Follow the established Compliance model structure
-- Include proper requirement mappings and metadata
+- Type hints required for all public functions
+- Docstrings required for classes and methods (Google style)
+- PEP 8 compliance enforced by black/flake8
+- Import order: standard → third-party → local
---
-## Tech Stack
+## TECH STACK
-- **Language**: Python 3.9+
-- **Dependency Management**: Poetry 2+
-- **CLI Framework**: Custom argument parser with provider-specific subcommands
-- **Testing**: Pytest with extensive unit and integration tests
-- **Code Quality**: Pre-commit hooks for Black, Flake8, Pylint, Bandit for security scanning
+Python 3.9+ | Poetry 2+ | pytest | moto (AWS mocking) | Pre-commit hooks (black, flake8, pylint, bandit)
-## Commands
+---
-### Development Environment
-
-```bash
-# Core development setup
-poetry install --with dev # Install all dependencies
-poetry run pre-commit install # Install pre-commit hooks
-
-# Code quality
-poetry run pre-commit run --all-files
-
-# Run tests
-poetry run pytest -n auto -vvv -s -x tests/
-```
-
-### Running Prowler CLI
-
-```bash
-# Run Prowler
-poetry run python prowler-cli.py --help
-
-# Run Prowler with a specific provider
-poetry run python prowler-cli.py
-
-# Run Prowler with error logging
-poetry run python prowler-cli.py --log-level ERROR --verbose
-
-# Run specific checks
-poetry run python prowler-cli.py --checks
-```
-
-## Project Structure
+## PROJECT STRUCTURE
```
prowler/
-├── __main__.py # Main CLI entry point
-├── config/ # Global configuration
-│ ├── config.py # Core configuration settings
-│ └── __init__.py
-├── lib/ # Core library functions
-│ ├── check/ # Check execution engine
-│ │ ├── check.py # Check execution logic
-│ │ ├── checks_loader.py # Dynamic check loading
-│ │ ├── compliance.py # Compliance framework handling
-│ │ └── models.py # Check and report models
-│ ├── cli/ # Command-line interface
-│ │ └── parser.py # Argument parsing
-│ ├── outputs/ # Output format handlers
-│ │ ├── csv/ # CSV output
-│ │ ├── html/ # HTML reports
-│ │ ├── json/ # JSON formats
-│ │ └── compliance/ # Compliance reports
-│ ├── scan/ # Scan orchestration
-│ ├── utils/ # Utility functions
-│ └── mutelist/ # Mute list functionality
-├── providers/ # Cloud provider implementations
-│ ├── aws/ # AWS provider
-│ ├── azure/ # Azure provider
-│ ├── gcp/ # Google Cloud provider
-│ ├── kubernetes/ # Kubernetes provider
-│ ├── github/ # GitHub provider
-│ ├── m365/ # Microsoft 365 provider
-│ ├── mongodbatlas/ # MongoDB Atlas provider
-│ ├── oci/ # Oracle Cloud provider
-│ ├── ...
-│ └── common/ # Shared provider utilities
-├── compliance/ # Compliance framework definitions
-│ ├── aws/ # AWS compliance frameworks
-│ ├── azure/ # Azure compliance frameworks
-│ ├── gcp/ # GCP compliance frameworks
-│ ├── ...
-└── exceptions/ # Global exception definitions
+├── __main__.py # CLI entry point
+├── config/ # Global configuration
+├── lib/
+│ ├── check/ # Check execution engine
+│ ├── cli/ # Command-line interface
+│ ├── outputs/ # Output format handlers (JSON, CSV, HTML, ASFF, OCSF)
+│ └── mutelist/ # Mute list functionality
+├── providers/ # Cloud providers (aws, azure, gcp, kubernetes, github, m365...)
+│ └── common/ # Shared provider utilities
+├── compliance/ # Compliance framework definitions (CIS, NIST, PCI-DSS, SOC2...)
+└── exceptions/ # Global exceptions
```
-## Key Components
+---
-### 1. Provider System
+## COMMANDS
-Each cloud provider implements:
-
-```python
-class Provider:
- """Base provider class"""
-
- def __init__(self, arguments):
- self.session = self._setup_session(arguments)
- self.regions = self._get_regions()
- # Initialize all services
-
- def _setup_session(self, arguments):
- """Provider-specific authentication"""
- pass
-
- def _get_regions(self):
- """Get available regions for provider"""
- pass
-```
-
-### 2. Check Engine
-
-The check execution system:
-
-- **Dynamic Loading**: Automatically discovers and loads checks
-- **Parallel Execution**: Runs checks in parallel for performance
-- **Error Isolation**: Individual check failures don't affect others
-- **Comprehensive Reporting**: Detailed findings with remediation guidance
-
-### 3. Compliance Framework Engine
-
-Compliance frameworks are defined as JSON files mapping checks to requirements:
-
-```json
-{
- "Framework": "CIS",
- "Name": "CIS Amazon Web Services Foundations Benchmark v2.0.0",
- "Version": "2.0",
- "Provider": "AWS",
- "Description": "The CIS Amazon Web Services Foundations Benchmark provides prescriptive guidance for configuring security options for a subset of Amazon Web Services with an emphasis on foundational, testable, and architecture agnostic settings.",
- "Requirements": [
- {
- "Id": "1.1",
- "Description": "Maintain current contact details",
- "Checks": ["account_contact_details_configured"]
- }
- ]
-}
-```
-
-### 4. Output System
-
-Multiple output formats supported:
-
-- **JSON**: Machine-readable findings
-- **CSV**: Spreadsheet-compatible format
-- **HTML**: Interactive web reports
-- **ASFF**: AWS Security Finding Format
-- **OCSF**: Open Cybersecurity Schema Framework
-
-## Development Patterns
-
-### Adding New Cloud Providers
-
-1. **Create Provider Structure**:
```bash
-mkdir -p prowler/providers/{provider}
-mkdir -p prowler/providers/{provider}/services
-mkdir -p prowler/providers/{provider}/lib/{service,arguments,mutelist}
-mkdir -p prowler/providers/{provider}/exceptions
+# Setup
+poetry install --with dev
+poetry run pre-commit install
+
+# Run Prowler
+poetry run python prowler-cli.py {provider}
+poetry run python prowler-cli.py {provider} --check {check_name}
+poetry run python prowler-cli.py {provider} --list-checks
+
+# Testing
+poetry run pytest -n auto -vvv tests/
+poetry run pytest tests/providers/{provider}/services/{service}/ -v
+
+# Code Quality
+poetry run pre-commit run --all-files
```
-2. **Implement Provider Class**:
-```python
-from prowler.providers.common.provider import Provider
+---
-class NewProvider(Provider):
- def __init__(self, arguments):
- super().__init__(arguments)
- # Provider-specific initialization
-```
+## CREATING NEW CHECKS (Quick Reference)
-3. **Add Provider to CLI**:
-Update `prowler/lib/cli/parser.py` to include new provider arguments.
+1. Verify check doesn't exist: `--list-checks | grep {check_name}`
+2. Create folder: `prowler/providers/{provider}/services/{service}/{check_name}/`
+3. Create files: `__init__.py`, `{check_name}.py`, `{check_name}.metadata.json`
+4. Implement check logic
+5. Test locally: `--check {check_name}`
+6. Write tests
-### Adding New Security Checks
+**For detailed guidance, use the `prowler-sdk-check` skill.**
-The most common high level steps to create a new check are:
+---
-1. Prerequisites:
- - Verify the check does not already exist by searching in the same service folder as `prowler/providers//services///`.
- - Ensure required provider and service exist. If not, you will need to create them first.
- - Confirm the service has implemented all required methods and attributes for the check (in most cases, you will need to add or modify some methods in the service to get the data you need for the check).
-2. Navigate to the service directory. The path should be as follows: `prowler/providers//services/`.
-3. Create a check-specific folder. The path should follow this pattern: `prowler/providers//services//`. Adhere to the [Naming Format for Checks](/developer-guide/checks#naming-format-for-checks).
-4. Create the check files, you can use next commands:
-```bash
-mkdir -p prowler/providers//services//
-touch prowler/providers//services///__init__.py
-touch prowler/providers//services///.py
-touch prowler/providers//services///.metadata.json
-```
-5. Run the check locally to ensure it works as expected. For checking you can use the CLI in the next way:
- - To ensure the check has been detected by Prowler: `poetry run python prowler-cli.py --list-checks | grep `.
- - To run the check, to find possible issues: `poetry run python prowler-cli.py --log-level ERROR --verbose --check `.
-6. Create comprehensive tests for the check that cover multiple scenarios including both PASS (compliant) and FAIL (non-compliant) cases. For detailed information about test structure and implementation guidelines, refer to the [Testing](/developer-guide/unit-testing) documentation.
-7. If the check and its corresponding tests are working as expected, you can submit a PR to Prowler.
+## QA CHECKLIST
-### Adding Compliance Frameworks
-
-1. **Create Framework File**:
-```bash
-# Create prowler/compliance/{provider}/{framework}.json
-```
-
-2. **Define Requirements**:
-Map framework requirements to existing checks.
-
-3. **Test Compliance**:
-```bash
-poetry run python -m prowler {provider} --compliance {framework}
-```
-
-## Code Quality Standards
-
-### 1. Python Style
-
-- **PEP 8 Compliance**: Enforced by black and flake8
-- **Type Hints**: Required for all public functions
-- **Docstrings**: Required for all classes and methods
-- **Import Organization**: Use isort for consistent import ordering
-
-```python
-import standard_library
-
-from third_party import library
-
-from prowler.lib import internal_module
-
-class ExampleClass:
- """Class docstring."""
-
- def method(self, param: str) -> dict | list | None:
- """Method docstring.
-
- Args:
- param: Description of parameter
-
- Returns:
- Description of return value
- """
- return None
-```
-
-### 2. Error Handling
-
-```python
-from prowler.lib.logger import logger
-
-try:
- # Risky operation
- result = api_call()
-except ProviderSpecificException as e:
- logger.error(f"Provider error: {e}")
- # Graceful handling
-except Exception as e:
- logger.error(f"Unexpected error: {e}")
- # Never let checks crash the entire scan
-```
-
-### 3. Security Practices
-
-- **No Hardcoded Secrets**: Use environment variables or secure credential management
-- **Input Validation**: Validate all external inputs
-- **Principle of Least Privilege**: Request minimal necessary permissions
-- **Secure Defaults**: Default to secure configurations
-
-## Testing Guidelines
-
-### Unit Tests
-
-- **100% Coverage Goal**: Aim for complete test coverage
-- **Mock External Services**: Use mock objects to simulate the external services
-- **Test Edge Cases**: Include error conditions and boundary cases
-
-## References
-
-- **Root Project Guide**: `../AGENTS.md` (takes priority for cross-component guidance)
-- **Provider Examples**: Reference existing providers for implementation patterns
-- **Check Examples**: Study existing checks for proper implementation patterns
-- **Compliance Framework Examples**: Review existing frameworks for structure
+- [ ] `poetry run pytest` passes
+- [ ] `poetry run pre-commit run --all-files` passes
+- [ ] Check metadata JSON is valid
+- [ ] Tests cover PASS, FAIL, and empty resource scenarios
+- [ ] Docstrings follow Google style
diff --git a/prowler/CHANGELOG.md b/prowler/CHANGELOG.md
index dbf0711422..0f96bbfd6c 100644
--- a/prowler/CHANGELOG.md
+++ b/prowler/CHANGELOG.md
@@ -5,6 +5,7 @@ All notable changes to the **Prowler SDK** are documented in this file.
## [5.17.0] (Prowler UNRELEASED)
### Added
+- AI Skills pack for AI coding assistants (Claude Code, OpenCode, Codex) following agentskills.io standard [(#9728)](https://github.com/prowler-cloud/prowler/pull/9728)
- Add Prowler ThreatScore for the Alibaba Cloud provider [(#9511)](https://github.com/prowler-cloud/prowler/pull/9511)
- `compute_instance_group_multiple_zones` check for GCP provider [(#9566)](https://github.com/prowler-cloud/prowler/pull/9566)
- `compute_instance_group_autohealing_enabled` check for GCP provider [(#9690)](https://github.com/prowler-cloud/prowler/pull/9690)
@@ -12,7 +13,13 @@ All notable changes to the **Prowler SDK** are documented in this file.
- `compute_instance_disk_auto_delete_disabled` check for GCP provider [(#9604)](https://github.com/prowler-cloud/prowler/pull/9604)
- Bedrock service pagination [(#9606)](https://github.com/prowler-cloud/prowler/pull/9606)
- `ResourceGroup` field to all check metadata for resource classification [(#9656)](https://github.com/prowler-cloud/prowler/pull/9656)
+- `compute_configuration_changes` check for GCP provider to detect Compute Engine configuration changes in Cloud Audit Logs [(#9698)](https://github.com/prowler-cloud/prowler/pull/9698)
- `compute_instance_group_load_balancer_attached` check for GCP provider [(#9695)](https://github.com/prowler-cloud/prowler/pull/9695)
+- `compute_instance_single_network_interface` check for GCP provider [(#9702)](https://github.com/prowler-cloud/prowler/pull/9702)
+- `compute_image_not_publicly_shared` check for GCP provider [(#9718)](https://github.com/prowler-cloud/prowler/pull/9718)
+- CIS 1.12 compliance framework for Kubernetes [(#9778)](https://github.com/prowler-cloud/prowler/pull/9778)
+- CIS 6.0 for M365 provider [(#9779)](https://github.com/prowler-cloud/prowler/pull/9779)
+- CIS 5.0 compliance framework for the Azure provider [(#9777)](https://github.com/prowler-cloud/prowler/pull/9777)
### Changed
- Update AWS Step Functions service metadata to new format [(#9432)](https://github.com/prowler-cloud/prowler/pull/9432)
@@ -32,6 +39,21 @@ All notable changes to the **Prowler SDK** are documented in this file.
- Update AWS SES service metadata to new format [(#9411)](https://github.com/prowler-cloud/prowler/pull/9411)
- Update AWS SSM Incidents service metadata to new format [(#9431)](https://github.com/prowler-cloud/prowler/pull/9431)
- Update AWS WorkSpaces service metadata to new format [(#9483)](https://github.com/prowler-cloud/prowler/pull/9483)
+- Update AWS OpenSearch service metadata to new format [(#9383)](https://github.com/prowler-cloud/prowler/pull/9383)
+- Update AWS VPC service metadata to new format [(#9479)](https://github.com/prowler-cloud/prowler/pull/9479)
+- Update AWS Transfer service metadata to new format [(#9434)](https://github.com/prowler-cloud/prowler/pull/9434)
+- Update AWS S3 service metadata to new format [(#9552)](https://github.com/prowler-cloud/prowler/pull/9552)
+- Update AWS DataSync service metadata to new format [(#8854)](https://github.com/prowler-cloud/prowler/pull/8854)
+- Update AWS RDS service metadata to new format [(#9551)](https://github.com/prowler-cloud/prowler/pull/9551)
+- Update AWS Bedrock service metadata to new format [(#8827)](https://github.com/prowler-cloud/prowler/pull/8827)
+
+---
+
+## [5.16.2] (Prowler v5.16.2) (UNRELEASED)
+
+### Fixed
+- Fix OCI authentication error handling and validation [(#9738)](https://github.com/prowler-cloud/prowler/pull/9738)
+- Fixup AWS EC2 SG library [(#9216)](https://github.com/prowler-cloud/prowler/pull/9216)
---
diff --git a/prowler/compliance/azure/cis_5.0_azure.json b/prowler/compliance/azure/cis_5.0_azure.json
new file mode 100644
index 0000000000..4a7172de77
--- /dev/null
+++ b/prowler/compliance/azure/cis_5.0_azure.json
@@ -0,0 +1,3431 @@
+{
+ "Framework": "CIS",
+ "Name": "CIS Microsoft Azure Foundations Benchmark v5.0.0",
+ "Version": "5.0",
+ "Provider": "Azure",
+ "Description": "The CIS Azure Foundations Benchmark provides prescriptive guidance for configuring security options for a subset of Azure with an emphasis on foundational, testable, and architecture agnostic settings.",
+ "Requirements": [
+ {
+ "Id": "2.1.1",
+ "Description": "Ensure that Azure Databricks is deployed in a customer-managed virtual network (VNet)",
+ "Checks": [
+ "databricks_workspace_vnet_injection_enabled"
+ ],
+ "Attributes": [
+ {
+ "Section": "2 Analytics Services",
+ "SubSection": "2.1 Azure Databricks",
+ "Profile": "Level 1",
+ "AssessmentStatus": "Automated",
+ "Description": "Networking for Azure Databricks can be set up in a few different ways. Using a customer-managed Virtual Network (VNet) (also known as VNet Injection) ensures that compute clusters and control planes are securely isolated within the organizations network boundary. By default, Databricks creates a managed VNet, which provides limited control over network security policies, firewall configurations, and routing.",
+ "RationaleStatement": "Using a customer-managed VNet ensures better control over network security and aligns with zero-trust architecture principles. It allows for: - Restricted outbound internet access to prevent unauthorized data exfiltration. - Integration with on-premises networks via VPN or ExpressRoute for hybrid connectivity. - Fine-grained NSG policies to restrict access at the subnet level. - Private Link for secure API access, avoiding public internet exposure.",
+ "ImpactStatement": "- Requires additional configuration during Databricks workspace deployment. - Might increase operational overhead for network maintenance. - May impact connectivity if misconfigured (e.g., restrictive NSG rules or missing routes).",
+ "RemediationProcedure": "**Remediate from Azure Portal** 1. Delete the existing Databricks workspace (migration required). 1. Create a new Databricks workspace with VNet Injection: 1. Go to Azure Portal Create Databricks Workspace. 1. Select Advanced Networking. 1. Choose Deploy into your own Virtual Network. 1. Specify a customer-managed VNet and associated subnets. 1. Enable Private Link for secure API access. **Remediate from Azure CLI** Deploy a new Databricks workspace in a custom VNet: ``` az databricks workspace create --name \\ --resource-group \\ --location \\ --managed-resource-group \\ --enable-no-public-ip true \\ --network-security-group-rule NoAzureServices \\ --public-network-access Disabled \\ --custom-virtual-network-id /subscriptions//resourceGroups//providers/Microsoft.Network/virtualNetworks/ ``` Ensure NSG Rules are correctly configured: ``` az network nsg rule create --resource-group \\ --nsg-name \\ --name DenyAllOutbound \\ --direction Outbound \\ --access Deny \\ --priority 4096 ``` **Remediate from PowerShell** ``` New-AzDatabricksWorkspace -ResourceGroupName -Name -Location -ManagedResourceGroupName -CustomVirtualNetworkId /subscriptions//resourceGroups//providers/Microsoft.Network/virtualNetworks/ ```",
+ "AuditProcedure": "**Audit from Azure Portal** 1. Go to Azure Portal Search for Databricks Workspaces. 1. Select the Databricks Workspace to audit. 1. Under Networking, check if the workspace is deployed in a Customer-Managed VNet. 1. If the Virtual Network field shows Databricks-Managed VNet, it is non-compliant. 1. Verify NSG rules and Private Endpoints for fine-grained access control. **Audit from Azure CLI** Run the following command to check if Databricks is using a customer-managed VNet: ``` az network vnet show --resource-group --name ``` Ensure that Databricks subnets are present in the VNet configuration. Validate NSG rules attached to the Databricks subnets. **Audit from PowerShell** ``` Get-AzDatabricksWorkspace -ResourceGroupName -Name | Select-Object VirtualNetworkId ``` If VirtualNetworkId is null or shows a Databricks-Managed VNet, it is non-compliant. **Audit from Azure Policy** If referencing a digital copy of this Benchmark, clicking a Policy ID will open a link to the associated Policy definition in Azure. If referencing a printed copy, you can search Policy IDs from this URL: https://portal.azure.com/#view/Microsoft_Azure_Policy/PolicyMenuBlade/~/Definitions - **Policy ID:** [9c25c9e4-ee12-4882-afd2-11fb9d87893f](https://portal.azure.com/#view/Microsoft_Azure_Policy/PolicyDetailBlade/definitionId/%2Fproviders%2FMicrosoft.Authorization%2FpolicyDefinitions%9c25c9e4-ee12-4882-afd2-11fb9d87893f) **- Name:** 'Azure Databricks Workspaces should be in a virtual network'",
+ "AdditionalInformation": "",
+ "References": "",
+ "DefaultValue": "By default, Azure Databricks uses a Databricks-Managed VNet."
+ }
+ ]
+ },
+ {
+ "Id": "2.1.2",
+ "Description": "Ensure that network security groups are configured for Databricks subnets",
+ "Checks": [],
+ "Attributes": [
+ {
+ "Section": "2 Analytics Services",
+ "SubSection": "2.1 Azure Databricks",
+ "Profile": "Level 1",
+ "AssessmentStatus": "Automated",
+ "Description": "Network Security Groups (NSGs) should be implemented to control inbound and outbound traffic to Azure Databricks subnets, ensuring only authorized communication. NSGs should be configured with deny rules to block unwanted traffic and restrict communication to essential sources only.",
+ "RationaleStatement": "Using NSGs with both explicit allow and deny rules provides clear documentation and control over permitted and prohibited traffic. While Azure NSGs implicitly deny all traffic not explicitly allowed, defining explicit deny rules for known malicious or unnecessary sources enhances clarity, simplifies troubleshooting, and supports compliance audits.",
+ "ImpactStatement": "* NSGs require periodic maintenance to ensure rule accuracy. * Misconfigured NSGs could inadvertently block required traffic.",
+ "RemediationProcedure": "**Remediate from Azure Portal** 1. Assign NSG to Databricks subnets under Networking > NSG Settings.",
+ "AuditProcedure": "**Audit from Azure Portal** 1. Navigate to Virtual Networks > Subnets, and review NSG assignments. **Audit from Azure CLI** ``` az network nsg list --query [].{Name:name, Rules:securityRules} ``` **Audit from PowerShell** ``` Get-AzNetworkSecurityGroup -ResourceGroupName ```",
+ "AdditionalInformation": "",
+ "References": "https://learn.microsoft.com/en-us/security/benchmark/azure/baselines/azure-databricks-security-baseline:https://learn.microsoft.com/en-us/azure/databricks/security/network/classic/vnet-inject#network-security-group-rules",
+ "DefaultValue": "By default, Databricks subnets do not have NSGs assigned."
+ }
+ ]
+ },
+ {
+ "Id": "2.1.3",
+ "Description": "Ensure that traffic is encrypted between cluster worker nodes",
+ "Checks": [],
+ "Attributes": [
+ {
+ "Section": "2 Analytics Services",
+ "SubSection": "2.1 Azure Databricks",
+ "Profile": "Level 2",
+ "AssessmentStatus": "Manual",
+ "Description": "By default, data exchanged between worker nodes in an Azure Databricks cluster is not encrypted. To ensure that data is encrypted at all times, whether at rest or in transit, you can create an initialization script that configures your clusters to encrypt traffic between worker nodes using AES 256-bit encryption over a TLS 1.3 connection.",
+ "RationaleStatement": "* Protects sensitive data during transit between cluster nodes, mitigating risks of data interception or unauthorized access. * Aligns with organizational security policies and compliance requirements that mandate encryption of data in transit. * Enhances overall security posture by ensuring that all inter-node communications within the cluster are encrypted.",
+ "ImpactStatement": "* Enabling encryption may introduce a performance penalty due to the computational overhead associated with encrypting and decrypting traffic. This can result in longer query execution times, especially for data-intensive operations. * Implementing encryption requires creating and managing init scripts, which adds complexity to cluster configuration and maintenance. * The shared encryption secret is derived from the hash of the keystore stored in DBFS. If the keystore is updated or rotated, all running clusters must be restarted to prevent authentication failures between Spark workers and drivers.",
+ "RemediationProcedure": "Create a JKS keystore: 1. Generate a Java KeyStore (JKS) file that will be used for SSL/TLS encryption. 2. Upload the keystore file to a secure directory in DBFS (e.g. /dbfs//jetty_ssl_driver_keystore.jks). Develop an init script: 3. Create an init script that performs the following tasks: - Retrieves the JKS keystore file and password. - Derives a shared encryption secret from the keystore. - Configures Spark driver and executor settings to enable encryption. 4. Example init script: ``` #!/bin/bash set -euo pipefail keystore_dbfs_file=/dbfs//jetty_ssl_driver_keystore.jks max_attempts=30 while [ ! -f ${keystore_dbfs_file} ]; do if [ $max_attempts == 0 ]; then echo ERROR: Unable to find the file : $keystore_dbfs_file. Failing the script. exit 1 fi sleep 2s ((max_attempts--)) done sasl_secret=$(sha256sum $keystore_dbfs_file | cut -d' ' -f1) if [ -z ${sasl_secret} ]; then echo ERROR: Unable to derive the secret. Failing the script. exit 1 fi local_keystore_file=$DB_HOME/keys/jetty_ssl_driver_keystore.jks local_keystore_password=gb1gQqZ9ZIHS if [[ $DB_IS_DRIVER = TRUE ]]; then driver_conf=${DB_HOME}/driver/conf/spark-branch.conf echo Configuring driver conf at $driver_conf if [ ! -e $driver_conf ]; then echo spark.authenticate true >> $driver_conf echo spark.authenticate.secret $sasl_secret >> $driver_conf echo spark.authenticate.enableSaslEncryption true >> $driver_conf echo spark.network.crypto.enabled true >> $driver_conf echo spark.network.crypto.keyLength 256 >> $driver_conf echo spark.network.crypto.keyFactoryAlgorithm PBKDF2WithHmacSHA1 >> $driver_conf echo spark.io.encryption.enabled true >> $driver_conf echo spark.ssl.enabled true >> $driver_conf echo spark.ssl.keyPassword $local_keystore_password >> $driver_conf echo spark.ssl.keyStore $local_keystore_file >> $driver_conf echo spark.ssl.keyStorePassword $local_keystore_password >> $driver_conf echo spark.ssl.protocol TLSv1.3 >> $driver_conf fi fi executor_conf=${DB_HOME}/conf/spark.executor.extraJavaOptions echo Configuring executor conf at $executor_conf if [ ! -e $executor_conf ]; then echo -Dspark.authenticate=true >> $executor_conf echo -Dspark.authenticate.secret=$sasl_secret >> $executor_conf echo -Dspark.authenticate.enableSaslEncryption=true >> $executor_conf echo -Dspark.network.crypto.enabled=true >> $executor_conf echo -Dspark.network.crypto.keyLength=256 >> $executor_conf echo -Dspark.network.crypto.keyFactoryAlgorithm=PBKDF2WithHmacSHA1 >> $executor_conf echo -Dspark.io.encryption.enabled=true >> $executor_conf echo -Dspark.ssl.enabled=true >> $executor_conf echo -Dspark.ssl.keyPassword=$local_keystore_password >> $executor_conf echo -Dspark.ssl.keyStore=$local_keystore_file >> $executor_conf echo -Dspark.ssl.keyStorePassword=$local_keystore_password >> $executor_conf echo -Dspark.ssl.protocol=TLSv1.3 >> $executor_conf fi ``` 5. Save.",
+ "AuditProcedure": "**Audit from Azure Portal** Review cluster init scripts: 1. Navigate to your Azure Databricks workspace, go to the Clusters section, select a cluster, and check the Advanced Options for any init scripts that configure encryption settings. Verify spark configuration: 2. Ensure that the following Spark configurations are set: ``` spark.authenticate true spark.authenticate.enableSaslEncryption true spark.network.crypto.enabled true spark.network.crypto.keyLength 256 spark.network.crypto.keyFactoryAlgorithm PBKDF2WithHmacSHA1 spark.io.encryption.enabled true ``` These settings can be found in the cluster's Spark configuration properties. Check keystone management: 3. Verify that the Java KeyStore (JKS) file is securely stored in DBFS and that its integrity is maintained. 4. Ensure that the keystore password is securely managed and not hardcoded in scripts.",
+ "AdditionalInformation": "",
+ "References": "https://learn.microsoft.com/en-us/azure/databricks/security/keys/encrypt-otw",
+ "DefaultValue": "By default, traffic is not encrypted between cluster worker nodes."
+ }
+ ]
+ },
+ {
+ "Id": "2.1.4",
+ "Description": "Ensure that users and groups are synced from Microsoft Entra ID to Azure Databricks",
+ "Checks": [],
+ "Attributes": [
+ {
+ "Section": "2 Analytics Services",
+ "SubSection": "2.1 Azure Databricks",
+ "Profile": "Level 1",
+ "AssessmentStatus": "Manual",
+ "Description": "To ensure centralized identity and access management, users and groups from Microsoft Entra ID should be synchronized with Azure Databricks. This is achieved through SCIM provisioning, which automates the creation, update, and deactivation of users and groups in Databricks based on Entra ID assignments. Enabling this integration ensures that access controls in Databricks remain consistent with corporate identity governance policies, reducing the risk of orphaned accounts, stale permissions, and unauthorized access.",
+ "RationaleStatement": "Syncing users and groups from Microsoft Entra ID centralizes access control, enforces the least privilege principle by automatically revoking unnecessary access, reduces administrative overhead by eliminating manual user management, and ensures auditability and compliance with industry regulations.",
+ "ImpactStatement": "SCIM provisioning requires role mapping to avoid misconfigured user privileges.",
+ "RemediationProcedure": "**Remediate from Azure Portal** Enable provisioning in Azure Portal: 1. Go to `Microsoft Entra ID`. 1. Under `Manage`, click `Enterprise applications`. 1. Click the name of the Azure Databricks SCIM application. 1. Under `Provisioning`, select `Automatic` and enter the SCIM endpoint and API token from Databricks. Enable provisioning in Databricks: 5. Navigate to `Admin Console` > `Identity and Access Management`. 6. Enable SCIM provisioning and generate an API token. Configure role assignments: 7. Ensure groups from Entra ID are mapped to appropriate Databricks roles. 8. Restrict administrative privileges to designated security groups. Regularly monitor sync logs: 9. Periodically review sync logs in Microsoft Entra ID and Databricks Admin Console. 10. Configure Azure Monitor alerts for provisioning failures. Disable manual user creation in Databricks: 11. Ensure that all user management is controlled via SCIM sync from Entra ID. 12. Disable personal access token usage for authentication. **Remediate from Azure CLI** Enable SCIM User and Group Provisioning in Azure Databricks: ``` az ad app update --id --set provisioning.provisioningMode=Automatic ```",
+ "AuditProcedure": "**Audit from Azure Portal** Verify SCIM provisioning is enabled: 1. Go to `Microsoft Entra ID`. 1. Under `Manage`, click `Enterprise applications`. 1. Click the name of the Azure Databricks SCIM application. 1. Under `Provisioning`, confirm that SCIM provisioning is enabled and running. Check user sync status in Azure Portal: 5. Under `Provisioning Logs`, verify the last successful sync and any failed entries. Check user sync status in Databricks: 6. Go to `Admin Console` > `Identity and Access Management`. 7. Confirm that Users and Groups match those assigned in Microsoft Entra ID. Ensure role-based access control (RBAC) mapping is correct: 8. Verify that users are assigned appropriate Databricks roles (e.g. Admin, User, Contributor). 9. Confirm that groups are mapped to workspace access roles.",
+ "AdditionalInformation": "",
+ "References": "https://learn.microsoft.com/en-us/azure/databricks/administration-guide/users-groups/scim/aad",
+ "DefaultValue": "By default, Azure Databricks does not sync users and groups from Microsoft Entra ID."
+ }
+ ]
+ },
+ {
+ "Id": "2.1.5",
+ "Description": "Ensure that Unity Catalog is configured for Azure Databricks",
+ "Checks": [],
+ "Attributes": [
+ {
+ "Section": "2 Analytics Services",
+ "SubSection": "2.1 Azure Databricks",
+ "Profile": "Level 1",
+ "AssessmentStatus": "Manual",
+ "Description": "Unity Catalog is a centralized governance model for managing and securing data in Azure Databricks. It provides fine-grained access control to databases, tables, and views using Microsoft Entra ID identities. Unity Catalog also enhances data lineage, audit logging, and compliance monitoring, making it a critical component for security and governance.",
+ "RationaleStatement": "* Enforces centralized access control policies and reduces data security risks. * Enables identity-based authentication via Microsoft Entra ID. * Improves compliance with industry regulations (e.g. GDPR, HIPAA, SOC 2) by providing audit logs and access visibility. * Prevents unauthorized data access through table-, row-, and column-level security (RLS & CLS).",
+ "ImpactStatement": "* Improperly configured permissions may lead to data exfiltration or unauthorized access. * Unity Catalog requires structured governance policies to be effective and prevent overly permissive access.",
+ "RemediationProcedure": "Use the remediation procedure written in this article: https://learn.microsoft.com/en-us/azure/databricks/data-governance/unity-catalog/get-started.",
+ "AuditProcedure": "Method 1: Verify unity catalog deployment: 1. As an Azure Databricks account admin, log into the account console. 1. Click Workspaces. 1. Find your workspace and check the Metastore column. If a metastore name is present, your workspace is attached to a Unity Catalog metastore and therefore enabled for Unity Catalog. Method 2: Run a SQL query to confirm Unity Catalog enablement Run the following SQL query in the SQL query editor or a notebook that is attached to a Unity Catalog-enabled compute resource. No admin role is required. ``` SELECT CURRENT_METASTORE(); ``` If the query returns a metastore ID like the following, then your workspace is attached to a Unity Catalog metastore and therefore enabled for Unity Catalog.",
+ "AdditionalInformation": "",
+ "References": "https://learn.microsoft.com/en-us/azure/databricks/data-governance/unity-catalog/:https://learn.microsoft.com/en-us/azure/databricks/admin/users-groups/:https://learn.microsoft.com/en-us/azure/databricks/data-governance/unity-catalog/enable-workspaces",
+ "DefaultValue": "New workspaces have Unity Catalog enabled by default. Existing workspaces may require manual enablement."
+ }
+ ]
+ },
+ {
+ "Id": "2.1.6",
+ "Description": "Ensure that usage is restricted and expiry is enforced for Databricks personal access tokens",
+ "Checks": [],
+ "Attributes": [
+ {
+ "Section": "2 Analytics Services",
+ "SubSection": "2.1 Azure Databricks",
+ "Profile": "Level 1",
+ "AssessmentStatus": "Manual",
+ "Description": "Databricks personal access tokens (PATs) provide API-based authentication for users and applications. By default, users can generate API tokens without expiration, leading to potential security risks if tokens are leaked, improperly stored, or not rotated regularly. To mitigate these risks, administrators should: * Restrict token creation to approved users and service principals. * Enforce expiration policies to prevent long-lived tokens. * Monitor token usage and revoke unused or compromised tokens.",
+ "RationaleStatement": "Restricting usage and enforcing expiry for personal access tokens reduces exposure to long-lived tokens, minimizes the risk of API abuse if compromised, and aligns with security best practices through controlled issuance and enforced expiry.",
+ "ImpactStatement": "If revoked improperly, applications relying on these tokens may fail, requiring a remediation plan for token rotation. Increased administrative effort is required to track and manage API tokens effectively.",
+ "RemediationProcedure": "**Remediate from Azure Portal** Disable personal access tokens: If your workspace does not require PATs, you can disable them entirely to prevent their use.",
+ "AuditProcedure": "Azure Databricks administrators can monitor and revoke personal access tokens within their workspace. Detailed instructions are available in the Monitor and Revoke Personal Access Tokens section of the Microsoft documentation: https://learn.microsoft.com/en-us/azure/databricks/admin/access-control/tokens. To evaluate the usage of personal access tokens in your Azure Databricks account, you can utilize the provided notebook that lists all PATs not rotated or updated in the last 90 days, allowing you to identify tokens that may require revocation. This process is detailed here: https://docs.azure.cn/en-us/databricks/security/auth/oauth-pat-usage. Implementing diagnostic logging provides a comprehensive reference of audit log services and events, enabling you to track activities related to personal access tokens. More information can be found in the diagnostic log reference section: https://docs.azure.cn/en-us/databricks/security/auth/oauth-pat-usage.",
+ "AdditionalInformation": "",
+ "References": "https://learn.microsoft.com/en-us/azure/databricks/administration-guide/access-control/tokens:https://learn.microsoft.com/en-us/azure/databricks/dev-tools/auth/",
+ "DefaultValue": "By default, personal access tokens are enabled and users can create the Personal access token and their expiry time."
+ }
+ ]
+ },
+ {
+ "Id": "2.1.7",
+ "Description": "Ensure that diagnostic log delivery is configured for Azure Databricks",
+ "Checks": [],
+ "Attributes": [
+ {
+ "Section": "2 Analytics Services",
+ "SubSection": "2.1 Azure Databricks",
+ "Profile": "Level 1",
+ "AssessmentStatus": "Automated",
+ "Description": "Azure Databricks Diagnostic Logging provides insights into system operations, user activities, and security events within a Databricks workspace. Enabling diagnostic logs helps organizations: * Detect security threats by logging access, job executions, and cluster activities. * Ensure compliance with industry regulations such as SOC 2, HIPAA, and GDPR. * Monitor operational performance and troubleshoot issues proactively.",
+ "RationaleStatement": "Diagnostic logging provides visibility into security and operational activities within Databricks workspaces while maintaining an audit trail for forensic investigations, and it supports compliance with regulatory standards that require logging and monitoring.",
+ "ImpactStatement": "Logs consume storage and may require additional monitoring tools, leading to increased operational overhead and costs. Incomplete log configurations may result in missing critical events, reducing monitoring effectiveness.",
+ "RemediationProcedure": "**Remediate from Azure Portal** Enable diagnostic logging for Azure Databricks: 1. Navigate to your Azure Databricks workspace. 1. In the left-hand menu, select `Monitoring` > `Diagnostic settings`. 1. Click `+ Add diagnostic setting`. 1. Under `Category details`, select the log categories you wish to capture, such as AuditLogs, Clusters, Notebooks, and Jobs. 1. Choose a destination for the logs: - `Log Analytics workspace`: For advanced querying and monitoring. - `Storage account`: For long-term retention. - `Event Hub`: For integration with third-party systems. 1. Provide a `Name` for the diagnostic setting. 1. Click `Save`. Implement log retention policies: 1. Navigate to your Log Analytics workspace. 1. Under `General`, select `Usage and estimated costs`. 1. Click `Data Retention`. 1. Adjust the retention period slider to the desired number of days (up to 730 days). 1. Click `OK`. Monitor logs for anomalies: 1. Navigate to `Azure Monitor`. 1. Select `Alerts` > `+ New alert rule`. 1. Under `Scope`, specify the Databricks resource. 1. Define `Condition` based on log queries that identify anomalies (e.g. unauthorized access attempts). 1. Configure `Actions` to notify stakeholders or trigger automated responses. 1. Provide an Alert rule `name` and `description`. 1. Click `Create alert rule`. **Remediate from Azure CLI** Enable diagnostic logging for Azure Databricks: ``` az monitor diagnostic-settings create --name DatabricksLogging --resource --logs '[{category: AuditLogs, enabled: true}, {category: Clusters, enabled: true}, {category: Notebooks, enabled: true}, {category: Jobs, enabled: true}]' --workspace ``` Implement log retention policies: ``` az monitor log-analytics workspace update --resource-group --name --retention-time 365 ``` Monitor logs for anomalies: ``` az monitor activity-log alert create --name DatabricksAnomalyAlert --resource-group --scopes --condition contains 'UnauthorizedAccess' ```",
+ "AuditProcedure": "**Audit from Azure Portal** Check if diagnostic logging is enabled for the Databricks workspace: 1. Go to `Azure Databricks`. 1. Select a workspace. 1. In the left-hand menu, select `Monitoring` > `Diagnostic settings`. 1. Verify if a diagnostic setting is configured. If not, diagnostic logging is not enabled. Ensure that logging is enabled for the following categories:",
+ "AdditionalInformation": "* Ensure that the Azure Databricks workspace is on the Premium plan to utilize diagnostic logging features. * Regularly review and update alert rules to adapt to evolving security threats and operational requirements.",
+ "References": "https://learn.microsoft.com/en-us/azure/databricks/admin/account-settings/audit-log-delivery:https://learn.microsoft.com/en-us/troubleshoot/azure/azure-monitor/log-analytics/billing/configure-data-retention",
+ "DefaultValue": ""
+ }
+ ]
+ },
+ {
+ "Id": "2.1.8",
+ "Description": "Ensure critical data in Azure Databricks is encrypted with customer-managed keys (CMK)",
+ "Checks": [
+ "storage_ensure_encryption_with_customer_managed_keys"
+ ],
+ "Attributes": [
+ {
+ "Section": "2 Analytics Services",
+ "SubSection": "2.1 Azure Databricks",
+ "Profile": "Level 2",
+ "AssessmentStatus": "Manual",
+ "Description": "Customer Managed Keys introduce additional depth to security by providing a means to manage access control for encryption keys. Where compliance and security frameworks indicate the need, and organizational capacity allows, sensitive data at rest can be encrypted using Customer Managed Keys (CMK) rather than Microsoft Managed keys.",
+ "RationaleStatement": "By default in Azure, data at rest tends to be encrypted using Microsoft Managed Keys. If your organization wants to control and manage encryption keys for compliance and defense-in-depth, Customer Managed Keys can be established. While it is possible to automate the assessment of this recommendation, the assessment status for this recommendation remains 'Manual' due to ideally limited scope. The scope of application - which workloads CMK is applied to - should be carefully considered to account for organizational capacity and targeted to workloads with specific need for CMK.",
+ "ImpactStatement": "If the key expires due to setting the 'activation date' and 'expiration date', the key must be rotated manually. Using Customer Managed Keys may also incur additional man-hour requirements to create, store, manage, and protect the keys as needed.",
+ "RemediationProcedure": "NOTE: These remediations assume that an Azure KeyVault already exists in the subscription. Remediate from Azure CLI 1. Create a dedicated key: az keyvault key create --vault-name --name -protection 2. Assign permissions to Databricks: az keyvault set-policy --name --resource-group --spn --key-permissions get wrapKey unwrapKey 3. Enable encryption with CMK: az databricks workspace update --name --resourcegroup --key-source Microsoft.KeyVault --key-name --keyvault-uri Remediate from PowerShell $Key = Add-AzKeyVaultKey -VaultName -Name Destination Set-AzDatabricksWorkspace -ResourceGroupName WorkspaceName -EncryptionKeySource Microsoft.KeyVault -KeyVaultUri $Key.Id",
+ "AuditProcedure": "Audit: Audit from Azure Portal 1. Go to Azure Portal → Databricks Workspaces. 2. Select a Databricks Workspace and go to Encryption settings. 3. Check if customer-managed keys (CMK) are enabled under Managed Disk Encryption .4. If CMK is not enabled, the workspace is non-compliant. Audit from Azure CLI Run the following command to check encryption settings for Databricks workspace: az databricks workspace show --name --resourcegroup --query encryption Ensure that keySource is set to Microsoft.KeyVault. Audit from PowerShell Get-AzDatabricksWorkspace -ResourceGroupName -Name | Select-Object Encryption Verify that encryption is set to Customer-Managed Keys (CMK). Audit from Databricks CLI databricks workspace get-metadata --workspace-id Ensure that encryption settings reflect a CMK setup.",
+ "AdditionalInformation": "This recommendation is based on the Common Reference Recommendation Ensure critical data is encrypted with customer-managed keys (CMK).",
+ "References": "https://docs.microsoft.com/en-us/azure/security/fundamentals/data-encryption-best-practices#protect-data-at-rest:https://learn.microsoft.com/en-us/security/benchmark/azure/mcsb-data-protection#dp-5-use-customer-managed-key-option-in-data-at-rest-encryption-when-required",
+ "DefaultValue": "By default, Encryption type is set to Microsoft Managed Keys."
+ }
+ ]
+ },
+ {
+ "Id": "2.1.9",
+ "Description": "Ensure 'No Public IP' is set to 'Enabled'",
+ "Checks": [
+ "databricks_workspace_vnet_injection_enabled"
+ ],
+ "Attributes": [
+ {
+ "Section": "2 Analytics Services",
+ "SubSection": "2.1 Azure Databricks",
+ "Profile": "Level 1",
+ "AssessmentStatus": "Automated",
+ "Description": "Enable secure cluster connectivity (also known as no public IP) on Azure Databricks workspaces to ensure that clusters do not have public IP addresses and communicate with the control plane over a secure connection.",
+ "RationaleStatement": "Enabling secure cluster connectivity limits exposure to the public internet, improving security and reducing the risk of external attacks.",
+ "ImpactStatement": "Enabling secure cluster connectivity requires careful network configuration. Before secure cluster connectivity can be enabled, Azure Databricks workspaces must be deployed in a customer-managed virtual network (VNet injection).",
+ "RemediationProcedure": "**Remediate from Azure Portal** 1. Go to `Azure Databricks`. 2. Click the name of a workspace. 3. Under `Settings`, click `Networking`. 4. Under `Network access`, next to `Deploy Azure Databricks workspace with Secure Cluster Connectivity (No Public IP)`, click the radio button next to `Enabled`. 5. Click `Save`. 6. Repeat steps 1-5 for each workspace requiring remediation. **Remediate from Azure CLI** For each workspace requiring remediation, run the following command to set enableNoPublicIp to true: ``` az databricks workspace update --resource-group --name --enable-no-public-ip true ``` **Remediate from PowerShell** For each workspace requiring remediation, run the following command to set EnableNoPublicIP to True: ``` Update-AzDatabricksWorkspace -ResourceGroupName -Name -EnableNoPublicIP ```",
+ "AuditProcedure": "**Audit from Azure Portal** 1. Go to `Azure Databricks`. 2. Click the name of a workspace. 3. Under `Settings`, click `Networking`. 4. Under `Network access`, ensure that `Deploy Azure Databricks workspace with Secure Cluster Connectivity (No Public IP)` is set to `Enabled`. 5. Repeat steps 1-4 for each workspace. **Audit from Azure CLI** Run the following command to list workspaces: ``` az databricks workspace list ``` For each workspace, run the following command to get the enableNoPublicIp setting: ``` az databricks workspace show --resource-group --name --query parameters.enableNoPublicIp.value ``` Ensure that `true` is returned. **Audit from PowerShell** Run the following command to list workspaces: ``` Get-AzDatabricksWorkspace ``` Run the following command to get the workspace in a resource group with a given name: ``` $workspace = Get-AzDatabricksWorkspace -ResourceGroupName -Name ``` Run the following command to get the EnableNoPublicIp setting: ``` $workspace.EnableNoPublicIP ``` Ensure that `True` is returned. **Audit from Azure Policy** - **Policy ID:** [51c1490f-3319-459c-bbbc-7f391bbed753] **- Name:** 'Azure Databricks Clusters should disable public IP'",
+ "AdditionalInformation": "",
+ "References": "https://learn.microsoft.com/en-us/azure/databricks/security/network/classic/secure-cluster-connectivity:https://learn.microsoft.com/en-us/cli/azure/databricks/workspace:https://learn.microsoft.com/en-us/powershell/module/az.databricks",
+ "DefaultValue": "No Public IP is set to Enabled by default."
+ }
+ ]
+ },
+ {
+ "Id": "2.1.10",
+ "Description": "Ensure 'Allow Public Network Access' is set to 'Disabled'",
+ "Checks": [],
+ "Attributes": [
+ {
+ "Section": "2 Analytics Services",
+ "SubSection": "2.1 Azure Databricks",
+ "Profile": "Level 1",
+ "AssessmentStatus": "Automated",
+ "Description": "Disable public network access to prevent exposure to the internet and reduce the risk of unauthorized access. Use private endpoints to securely manage access within trusted networks.",
+ "RationaleStatement": "Disabling public network access improves security by ensuring that Azure Databricks workspaces are not exposed on the public internet.",
+ "ImpactStatement": "Prior to disabling public network access, it is strongly recommended that virtual network integration is completed or private endpoints/links are set up. Disabling public network access restricts access to the service and will require the configuration of a virtual network and/or private endpoints for any services or users needing access within trusted networks.",
+ "RemediationProcedure": "**Remediate from Azure Portal** 1. Go to `Azure Databricks`. 2. Click the name of a workspace. 3. Under `Settings` click `Networking`. 4. Under `Network access`, next to `Allow Public Network Access`, click the radio button next to `Disabled`. 5. Click `Save`. 6. Repeat steps 1-5 for each workspace requiring remediation. **Remediate from Azure CLI** For each workspace requiring remediation, run the following command to set publicNetworkAccess to Disabled: ``` az databricks workspace update --resource-group --name --public-network-access Disabled ``` **Remediate from PowerShell** For each workspace requiring remediation, run the following command to set PublicNetworkAccess to Disabled: ``` Update-AzDatabricksWorkspace -ResourceGroupName -Name -PublicNetworkAccess Disabled ```",
+ "AuditProcedure": "**Audit from Azure Portal** 1. Go to `Azure Databricks`. 2. Click the name of a workspace. 3. Under `Settings` click `Networking`. 4. Under `Network access`, ensure `Allow Public Network Access` is set to `Disabled`. 5. Repeat steps 1-4 for each workspace. **Audit from Azure CLI** Run the following command to list workspaces: ``` az databricks workspace list ``` For each workspace, run the following command to get the publicNetworkAccess setting: ``` az databricks workspace show --resource-group --name --query publicNetworkAccess ``` Ensure that `Disabled` is returned. **Audit from PowerShell** Run the following command to list workspaces: ``` Get-AzDatabricksWorkspace ``` Run the following command to get the PublicNetworkAccess setting: ``` $workspace = Get-AzDatabricksWorkspace -ResourceGroupName -Name $workspace.PublicNetworkAccess ``` Ensure that `Disabled` is returned. **Audit from Azure Policy** - **Policy ID:** [0e7849de-b939-4c50-ab48-fc6b0f5eeba2] **- Name:** 'Azure Databricks Workspaces should disable public network access'",
+ "AdditionalInformation": "This recommendation is based on the Common Reference Recommendation Ensure public network access is Disabled.",
+ "References": "https://learn.microsoft.com/en-us/cli/azure/databricks/workspace:https://learn.microsoft.com/en-us/powershell/module/az.databricks",
+ "DefaultValue": "Allow Public Network Access is set to Enabled by default."
+ }
+ ]
+ },
+ {
+ "Id": "2.1.11",
+ "Description": "Ensure private endpoints are used to access Azure Databricks workspaces",
+ "Checks": [],
+ "Attributes": [
+ {
+ "Section": "2 Analytics Services",
+ "SubSection": "2.1 Azure Databricks",
+ "Profile": "Level 2",
+ "AssessmentStatus": "Automated",
+ "Description": "Use private endpoints for Azure Databricks workspaces to allow clients and services to securely access data located over a network via an encrypted Private Link. The private endpoint uses an IP address from the VNet for each service. Network traffic between disparate services securely traverses encrypted over the VNet.",
+ "RationaleStatement": "Using private endpoints for Azure Databricks workspaces ensures that all communication between clients, services, and data sources occurs over a secure, private IP space within an Azure Virtual Network (VNet). This approach eliminates exposure to the public internet, significantly reducing the attack surface and aligning with Zero Trust principles.",
+ "ImpactStatement": "If an Azure Virtual Network is not implemented correctly, this may result in the loss of critical network traffic. Private endpoints are charged per hour of use. Before a private endpoint can be configured, Azure Databricks workspaces must be deployed in a customer-managed virtual network, must have secure cluster connectivity enabled, and must be on the Premium pricing tier.",
+ "RemediationProcedure": "**Remediate from Azure Portal** 1. Go to `Azure Databricks`. 2. Click the name of a workspace. 3. Under `Settings`, click `Networking`. 4. Click `Private endpoint connections`. 5. Click `+ Private endpoint`. 6. Under `Project details`, select a Subscription and a Resource group. 7. Under `Instance details`, provide a Name, Network Interface Name, and select a Region. 8. Click `Next : Resource >`. 9. Select a Target sub-resource. 10. Click `Next : Virtual Network >`. 11. Under `Networking`, select a Virtual network and a Subnet. 12. Optionally, configure Private IP configuration and Application security group. 13. Click `Next : DNS >`. 14. Optionally, configure Private DNS integration. 15. Click `Next : Tags >`. 16. Optionally, configure tags. 17. Click `Next : Review + create >`. 18. Click `Create`. 19. Repeat steps 1-18 for each workspace requiring remediation. **Remediate from Azure CLI** For each workspace requiring remediation, run the following command to create a private endpoint connection: ``` az network private-endpoint create --resource-group --name --location --vnet-name --subnet --private-connection-resource-id --connection-name --group-id ```",
+ "AuditProcedure": "**Audit from Azure Portal** 1. Go to `Azure Databricks`. 2. Click the name of a workspace. 3. Under `Settings`, click `Networking`. 4. Click `Private endpoint connections`. 5. Ensure a private endpoint connection exists with a connection state of `Approved`. 6. Repeat steps 1-5 for each workspace. **Audit from Azure CLI** Run the following command to list workspaces: ``` az databricks workspace list ``` For each workspace, run the following command to get the privateEndpointConnections configuration: ``` az databricks workspace show --resource-group --name --query privateEndpointConnections ``` Ensure a private endpoint connection is returned with a privateLinkServiceConnectionState status of `Approved`. **Audit from PowerShell** Run the following command to list workspaces: ``` Get-AzDatabricksWorkspace ``` Run the following command to get the PrivateEndpointConnection configuration: ``` $workspace = Get-AzDatabricksWorkspace -ResourceGroupName -Name $workspace.PrivateEndpointConnection | Select-Object -Property Id,PrivateLinkServiceConnectionStateStatus ``` Ensure a private endpoint connection is returned with a PrivateLinkServiceConnectionStateStatus of `Approved`. **Audit from Azure Policy** - **Policy ID:** [258823f2-4595-4b52-b333-cc96192710d8] **- Name:** 'Azure Databricks Workspaces should use private link'",
+ "AdditionalInformation": "This recommendation is based on the Common Reference Recommendation Ensure Private Endpoints are used to access {service}.",
+ "References": "https://learn.microsoft.com/en-us/azure/databricks/security/network/classic/private-link:https://learn.microsoft.com/en-us/cli/azure/databricks/workspace:https://learn.microsoft.com/en-us/powershell/module/az.databricks",
+ "DefaultValue": "Private endpoints are not configured for Azure Databricks workspaces by default."
+ }
+ ]
+ },
+ {
+ "Id": "3.1.1",
+ "Description": "Ensure only MFA enabled identities can access privileged Virtual Machine",
+ "Checks": [
+ "entra_user_with_vm_access_has_mfa"
+ ],
+ "Attributes": [
+ {
+ "Section": "3 Compute Services",
+ "SubSection": "3.1 Virtual Machines",
+ "Profile": "Level 2",
+ "AssessmentStatus": "Manual",
+ "Description": "Verify identities without MFA that can log in to a privileged virtual machine using separate login credentials. An adversary can leverage the access to move laterally and perform actions with the virtual machine's managed identity. Make sure the virtual machine only has necessary permissions, and revoke the admin-level permissions according to the principle of least privilege.",
+ "RationaleStatement": "Integrating multi-factor authentication (MFA) as part of the organizational policy can greatly reduce the risk of an identity gaining control of valid credentials that may be used for additional tactics such as initial access, lateral movement, and collecting information. MFA can also be used to restrict access to cloud resources and APIs. An Adversary may log into accessible cloud services within a compromised environment using Valid Accounts that are synchronized to move laterally and perform actions with the virtual machine's managed identity. The adversary may then perform management actions or access cloud-hosted resources as the logged-on managed identity.",
+ "ImpactStatement": "This recommendation requires the Entra ID P2 license to implement. Ensure that identities provisioned to a virtual machine utilize an RBAC/ABAC group and are allocated a role using Azure PIM, and that the role settings require MFA or use another third-party PAM solution for accessing virtual machines.",
+ "RemediationProcedure": "**Remediate from Azure Portal** 1. Log in to the Azure portal. 2. This can be remediated by enabling MFA for user, Removing user access or Reducing access of managed identities attached to virtual machines. - Case I : Enable MFA for users having access on virtual machines. 1. Go to `Microsoft Entra ID`. 1. For `Per-user MFA`: 1. Under `Manage`, click `Users`. 1. Click `Per-user MFA`. 1. For each user requiring remediation, check the box next to their name. 1. Click `Enable MFA`. 1. Click `Enable`. 1. For `Conditional Access`: 1. Under `Manage`, click `Security`. 1. Under `Protect`, click `Conditional Access`. 1. Update the Conditional Access policy requiring MFA for all users, removing each user requiring remediation from the `Exclude` list. - Case II : Removing user access on a virtual machine. 1. Select the `Subscription`, then click on `Access control (IAM)`. 2. Select `Role assignments` and search for `Virtual Machine Administrator Login` or `Virtual Machine User Login` or any role that provides access to log into virtual machines. 3. Click on `Role Name`, Select `Assignments`, and remove identities with no MFA configured. - Case III : Reducing access of managed identities attached to virtual machines. 1. Select the `Subscription`, then click on `Access control (IAM)`. 2. Select `Role Assignments` from the top menu and apply filters on `Assignment type` as `Privileged administrator roles` and `Type` as `Virtual Machines`. 3. Click on `Role Name`, Select `Assignments`, and remove identities access make sure this follows the least privileges principal.",
+ "AuditProcedure": "**Audit from Azure Portal** 1. Log in to the Azure portal. 1. Select the `Subscription`, then click on `Access control (IAM)`. 1. Click `Role : All` and click `All` to display the drop-down menu. 1. Type `Virtual Machine Administrator Login` and select `Virtual Machine Administrator Login`. 1. Review the list of identities that have been assigned the `Virtual Machine Administrator Login` role. 1. Go to `Microsoft Entra ID`. 1. For `Per-user MFA`: 1. Under `Manage`, click `Users`. 1. Click `Per-user MFA`. 1. Ensure that none of the identities assigned the `Virtual Machine Administrator Login` role from step 4 have `Status` set to `disabled`. 1. For `Conditional Access`: 1. Under `Manage`, click `Security`. 1. Under `Protect`, click `Conditional Access`. 1. Ensure that none of the identities assigned the `Virtual Machine Administrator Login` role from step 4 are exempt from a Conditional Access policy requiring MFA for all users.",
+ "AdditionalInformation": "",
+ "References": "",
+ "DefaultValue": ""
+ }
+ ]
+ },
+ {
+ "Id": "5.1.1",
+ "Description": "Ensure that 'security defaults' is enabled in Microsoft Entra ID",
+ "Checks": [
+ "entra_security_defaults_enabled"
+ ],
+ "Attributes": [
+ {
+ "Section": "5 Identity Services",
+ "SubSection": "5.1 Security Defaults (Per-User MFA)",
+ "Profile": "Level 1",
+ "AssessmentStatus": "Automated",
+ "Description": "[**IMPORTANT - Please read the section overview:** If your organization pays for Microsoft Entra ID licensing (included in Microsoft 365 E3, E5, F5, or Business Premium, and EM&S E3 or E5 licenses) and **CAN** use Conditional Access, ignore the recommendations in this section and proceed to the Conditional Access section.] Security defaults in Microsoft Entra ID make it easier to be secure and help protect your organization. Security defaults contain preconfigured security settings for common attacks. Security defaults is available to everyone. The goal is to ensure that all organizations have a basic level of security enabled at no extra cost. You may turn on security defaults in the Azure portal.",
+ "RationaleStatement": "Security defaults provide secure default settings that we manage on behalf of organizations to keep customers safe until they are ready to manage their own identity security settings. For example, doing the following: - Requiring all users and admins to register for MFA. - Challenging users with MFA - when necessary, based on factors such as location, device, role, and task. - Disabling authentication from legacy authentication clients, which cant do MFA.",
+ "ImpactStatement": "This recommendation should be implemented initially and then may be overridden by other service/product specific CIS Benchmarks. Administrators should also be aware that certain configurations in Microsoft Entra ID may impact other Microsoft services such as Microsoft 365.",
+ "RemediationProcedure": "**Remediate from Azure Portal** To enable security defaults in your directory: 1. From Azure Home select the Portal Menu. 1. Browse to `Microsoft Entra ID` > `Properties`. 1. Select `Manage security defaults`. 1. Under `Security defaults`, select `Enabled (recommended)`. 1. Select `Save`.",
+ "AuditProcedure": "**Audit from Azure Portal** To ensure security defaults is enabled in your directory: 1. From Azure Home select the Portal Menu. 2. Browse to `Microsoft Entra ID` > `Properties`. 3. Select `Manage security defaults`. 4. Under `Security defaults`, verify that `Enabled (recommended)` is selected.",
+ "AdditionalInformation": "This recommendation differs from the [Microsoft 365 Benchmark](https://workbench.cisecurity.org/benchmarks/5741). This is because the potential impact associated with disabling Security Defaults is dependent upon the security settings implemented in the environment. It is recommended that organizations disabling Security Defaults implement appropriate security settings to replace the settings configured by Security Defaults.",
+ "References": "https://docs.microsoft.com/en-us/azure/active-directory/fundamentals/concept-fundamentals-security-defaults:https://techcommunity.microsoft.com/t5/azure-active-directory-identity/introducing-security-defaults/ba-p/1061414:https://learn.microsoft.com/en-us/security/benchmark/azure/mcsb-identity-management#im-2-protect-identity-and-authentication-systems",
+ "DefaultValue": "If your tenant was created on or after October 22, 2019, security defaults may already be enabled in your tenant."
+ }
+ ]
+ },
+ {
+ "Id": "5.1.2",
+ "Description": "Ensure that 'multifactor authentication' is 'enabled' for all users",
+ "Checks": [
+ "entra_privileged_user_has_mfa",
+ "entra_non_privileged_user_has_mfa"
+ ],
+ "Attributes": [
+ {
+ "Section": "5 Identity Services",
+ "SubSection": "5.1 Security Defaults (Per-User MFA)",
+ "Profile": "Level 1",
+ "AssessmentStatus": "Automated",
+ "Description": "[**IMPORTANT - Please read the section overview:** If your organization pays for Microsoft Entra ID licensing (included in Microsoft 365 E3, E5, F5, or Business Premium, and EM&S E3 or E5 licenses) and **CAN** use Conditional Access, ignore the recommendations in this section and proceed to the Conditional Access section.] Enable multifactor authentication for all users. **Note:** Since 2024, Azure has been rolling out mandatory multifactor authentication. For more information: - https://azure.microsoft.com/en-us/blog/announcing-mandatory-multi-factor-authentication-for-azure-sign-in - https://learn.microsoft.com/en-us/entra/identity/authentication/concept-mandatory-multifactor-authentication",
+ "RationaleStatement": "Multifactor authentication requires an individual to present a minimum of two separate forms of authentication before access is granted. Multifactor authentication provides additional assurance that the individual attempting to gain access is who they claim to be. With multifactor authentication, an attacker would need to compromise at least two different authentication mechanisms, increasing the difficulty of compromise and thus reducing the risk.",
+ "ImpactStatement": "Users would require two forms of authentication before any access is granted. Additional administrative time will be required for managing dual forms of authentication when enabling multifactor authentication.",
+ "RemediationProcedure": "**Remediate from Azure Portal** 1. Go to `Microsoft Entra ID`. 1. Under `Manage`, click `Users`. 1. Click `Per-user MFA` from the top menu. 1. Click the box next to a user with `Status` `disabled`. 1. Click `Enable MFA`. 1. Click `Enable`. 1. Repeat steps 1-6 for each user requiring remediation. **Other options within Azure Portal** - [https://docs.microsoft.com/en-us/azure/active-directory/authentication/tutorial-enable-azure-mfa](https://docs.microsoft.com/en-us/azure/active-directory/authentication/tutorial-enable-azure-mfa) - [https://docs.microsoft.com/en-us/azure/active-directory/authentication/howto-mfa-mfasettings](https://docs.microsoft.com/en-us/azure/active-directory/authentication/howto-mfa-mfasettings) - [https://docs.microsoft.com/en-us/azure/active-directory/conditional-access/howto-conditional-access-policy-admin-mfa](https://docs.microsoft.com/en-us/azure/active-directory/conditional-access/howto-conditional-access-policy-admin-mfa) - [https://docs.microsoft.com/en-us/azure/active-directory/authentication/howto-mfa-getstarted#enable-multi-factor-authentication-with-conditional-access](https://docs.microsoft.com/en-us/azure/active-directory/authentication/howto-mfa-getstarted#enable-multi-factor-authentication-with-conditional-access)",
+ "AuditProcedure": "**Audit from Azure Portal** 1. Go to `Microsoft Entra ID`. 1. Under `Manage`, click `Users`. 1. Click `Per-user MFA` from the top menu. 1. Ensure that `Status` is `enabled` for all users. **Audit from REST API** Run the following Graph PowerShell command: ``` get-mguser -All | where {$_.StrongAuthenticationMethods.Count -eq 0} | Select-Object -Property UserPrincipalName ``` If the output contains any `UserPrincipalName`, then this recommendation is non-compliant.",
+ "AdditionalInformation": "",
+ "References": "https://docs.microsoft.com/en-us/azure/multi-factor-authentication/multi-factor-authentication:https://learn.microsoft.com/en-us/entra/identity/authentication/concept-mandatory-multifactor-authentication:https://azure.microsoft.com/en-us/blog/announcing-mandatory-multi-factor-authentication-for-azure-sign-in/:https://learn.microsoft.com/en-us/security/benchmark/azure/mcsb-identity-management#im-4-authenticate-server-and-services",
+ "DefaultValue": "Multifactor authentication is not enabled for all users by default. Starting in 2024, multifactor authentication is enabled for administrative accounts by default."
+ }
+ ]
+ },
+ {
+ "Id": "5.1.3",
+ "Description": "Ensure that 'Allow users to remember multifactor authentication on devices they trust' is disabled",
+ "Checks": [],
+ "Attributes": [
+ {
+ "Section": "5 Identity Services",
+ "SubSection": "5.1 Security Defaults (Per-User MFA)",
+ "Profile": "Level 1",
+ "AssessmentStatus": "Manual",
+ "Description": "[**IMPORTANT - Please read the section overview:** If your organization pays for Microsoft Entra ID licensing (included in Microsoft 365 E3, E5, F5, or Business Premium, and EM&S E3 or E5 licenses) and **CAN** use Conditional Access, ignore the recommendations in this section and proceed to the Conditional Access section.] Do not allow users to remember multi-factor authentication on devices.",
+ "RationaleStatement": "Remembering Multi-Factor Authentication (MFA) for devices and browsers allows users to have the option to bypass MFA for a set number of days after performing a successful sign-in using MFA. This can enhance usability by minimizing the number of times a user may need to perform two-step verification on the same device. However, if an account or device is compromised, remembering MFA for trusted devices may affect security. Hence, it is recommended that users not be allowed to bypass MFA.",
+ "ImpactStatement": "For every login attempt, the user will be required to perform multi-factor authentication.",
+ "RemediationProcedure": "**Remediate from Azure Portal** 1. From Azure Home select the Portal Menu 1. Select `Microsoft Entra ID` 1. Under `Manage`, click `Users` 1. Click the `Per-user MFA` button on the top bar 1. Click on `Service settings` 1. Uncheck the box next to `Allow users to remember multi-factor authentication on devices they trust` 1. Click `Save`",
+ "AuditProcedure": "**Audit from Azure Portal** 1. From Azure Home select the Portal Menu 1. Select `Microsoft Entra ID` 1. Under `Manage`, click `Users` 1. Click the `Per-user MFA` button on the top bar 1. Click on `Service settings` 1. Ensure that `Allow users to remember multi-factor authentication on devices they trust` is not enabled",
+ "AdditionalInformation": "",
+ "References": "https://learn.microsoft.com/en-us/entra/identity/authentication/howto-mfa-mfasettings#remember-multi-factor-authentication-for-devices-that-users-trust:https://docs.microsoft.com/en-us/security/benchmark/azure/security-controls-v3-identity-management#im-4-use-strong-authentication-controls-for-all-azure-active-directory-based-access:https://learn.microsoft.com/en-us/security/benchmark/azure/mcsb-identity-management#im-6-use-strong-authentication-controls",
+ "DefaultValue": "By default, `Allow users to remember multi-factor authentication on devices they trust` is disabled."
+ }
+ ]
+ },
+ {
+ "Id": "5.2.1",
+ "Description": "Ensure that 'trusted locations' are defined",
+ "Checks": [
+ "entra_trusted_named_locations_exists"
+ ],
+ "Attributes": [
+ {
+ "Section": "5 Identity Services",
+ "SubSection": "5.2 Conditional Access",
+ "Profile": "Level 1",
+ "AssessmentStatus": "Manual",
+ "Description": "Microsoft Entra ID Conditional Access allows an organization to configure `Named locations` and configure whether those locations are trusted or untrusted. These settings provide organizations the means to specify Geographical locations for use in conditional access policies, or define actual IP addresses and IP ranges and whether or not those IP addresses and/or ranges are trusted by the organization.",
+ "RationaleStatement": "Defining trusted source IP addresses or ranges helps organizations create and enforce Conditional Access policies around those trusted or untrusted IP addresses and ranges. Users authenticating from trusted IP addresses and/or ranges may have less access restrictions or access requirements when compared to users that try to authenticate to Microsoft Entra ID from untrusted locations or untrusted source IP addresses/ranges.",
+ "ImpactStatement": "When configuring `Named locations`, the organization can create locations using Geographical location data or by defining source IP addresses or ranges. Configuring `Named locations` using a Country location does not provide the organization the ability to mark those locations as trusted, and any Conditional Access policy relying on those `Countries location` setting will not be able to use the `All trusted locations` setting within the Conditional Access policy. They instead will have to rely on the `Select locations` setting. This may add additional resource requirements when configuring and will require thorough organizational testing. In general, Conditional Access policies may completely prevent users from authenticating to Microsoft Entra ID, and thorough testing is recommended. To avoid complete lockout, a 'Break Glass' account with full Global Administrator rights is recommended in the event all other administrators are locked out of authenticating to Microsoft Entra ID. This 'Break Glass' account should be excluded from Conditional Access Policies and should be configured with the longest pass phrase feasible in addition to a FIDO2 security key or certificate kept in a very secure physical location. This account should only be used in the event of an emergency and complete administrator lockout. **NOTE:** Starting July 2024, Microsoft will begin requiring MFA for All Users - including Break Glass Accounts. By the end of October 2024, this requirement will be enforced. Physical FIDO2 security keys, or a certificate kept on secure removable storage can fulfill this MFA requirement. If opting for a physical device, that device should be kept in a very secure, documented physical location.",
+ "RemediationProcedure": "**Remediate from Azure Portal** 1. In the Azure Portal, navigate to `Microsoft Entra ID` 1. Under `Manage`, click `Security` 1. Under `Protect`, click `Conditional Access` 1. Under `Manage`, click `Named locations` 1. Within the `Named locations` blade, click on `IP ranges location` 1. Enter a name for this location setting in the `Name` text box 1. Click on the `+` sign 1. Add an IP Address Range in CIDR notation inside the text box that appears 1. Click on the `Add` button 1. Repeat steps 7 through 9 for each IP Range that needs to be added 1. If the information entered are trusted ranges, select the `Mark as trusted location` check box 1. Once finished, click on `Create` **Remediate from PowerShell** Create a new trusted IP-based Named location policy ``` [System.Collections.Generic.List`1[Microsoft.Open.MSGraph.Model.IpRange]]$ipRanges = @() $ipRanges.Add() $ipRanges.Add() $ipRanges.Add() New-MgIdentityConditionalAccessNamedLocation -dataType #microsoft.graph.ipNamedLocation -DisplayName -IsTrusted $true -IpRanges $ipRanges ``` Set an existing IP-based Named location policy to trusted ``` Update-MgIdentityConditionalAccessNamedLocation -PolicyId -dataType #microsoft.graph.ipNamedLocation -IsTrusted $true ```",
+ "AuditProcedure": "**Audit from Azure Portal** 1. In the Azure Portal, navigate to `Microsoft Entra ID` 1. Under `Manage`, click `Security` 1. Under `Protect`, click `Conditional Access` 1. Under `Manage`, click `Named locations` Ensure there are `IP ranges location` settings configured and marked as `Trusted` **Audit from PowerShell** ``` Get-MgIdentityConditionalAccessNamedLocation ``` In the output from the above command, for each Named location group, make sure at least one entry contains the `IsTrusted` parameter with a value of `True`. Otherwise, if there is no output as a result of the above command or all of the entries contain the `IsTrusted` parameter with an empty value, a `NULL` value, or a value of `False`, the results are out of compliance with this check.",
+ "AdditionalInformation": "",
+ "References": "https://learn.microsoft.com/en-us/entra/identity/conditional-access/concept-assignment-network:https://learn.microsoft.com/en-us/security/benchmark/azure/mcsb-identity-management#im-7-restrict-resource-access-based-on--conditions:https://learn.microsoft.com/en-us/entra/identity/role-based-access-control/security-emergency-access",
+ "DefaultValue": "By default, no locations are configured under the `Named locations` blade within the Microsoft Entra ID Conditional Access blade."
+ }
+ ]
+ },
+ {
+ "Id": "5.2.2",
+ "Description": "Ensure that an exclusionary geographic Conditional Access policy is considered",
+ "Checks": [
+ "entra_trusted_named_locations_exists"
+ ],
+ "Attributes": [
+ {
+ "Section": "5 Identity Services",
+ "SubSection": "5.2 Conditional Access",
+ "Profile": "Level 1",
+ "AssessmentStatus": "Manual",
+ "Description": "**CAUTION**: If these policies are created without first auditing and testing the result, misconfiguration can potentially lock out administrators or create undesired access issues. Conditional Access Policies can be used to block access from geographic locations that are deemed out-of-scope for your organization or application. The scope and variables for this policy should be carefully examined and defined.",
+ "RationaleStatement": "Conditional Access, when used as a deny list for the tenant or subscription, is able to prevent ingress or egress of traffic to countries that are outside of the scope of interest (e.g.: customers, suppliers) or jurisdiction of an organization. This is an effective way to prevent unnecessary and long-lasting exposure to international threats such as APTs.",
+ "ImpactStatement": "Microsoft Entra ID P1 or P2 is required. Limiting access geographically will deny access to users that are traveling or working remotely in a different part of the world. A point-to-site or site to site tunnel such as a VPN is recommended to address exceptions to geographic access policies.",
+ "RemediationProcedure": "**Remediate from Azure Portal** Part 1 of 2 - Create the policy and enable it in `Report-only` mode. 1. From Azure Home open the portal menu in the top left, and select `Microsoft Entra ID`. 1. Scroll down in the menu on the left, and select `Security`. 1. Select on the left side `Conditional Access`. 1. Select `Policies`. 1. Click the `+ New policy` button, then: 1. Provide a name for the policy. 1. Under `Assignments`, select `Users` then: - Under `Include`, select `All users` - Under `Exclude`, check Users and groups and only select emergency access accounts and service accounts (**NOTE**: Service accounts are excluded here because service accounts are non-interactive and cannot complete MFA) 1. Under `Assignments`, select `Target resources` then: - Under `Include`, select `All cloud apps` - Leave `Exclude` blank unless you have a well defined exception 1. Under `Conditions`, select `Locations` then: - Select `Include`, then add entries for locations for those that should be **blocked** - Select `Exclude`, then add entries for those that should be allowed (**IMPORTANT**: Ensure that all Trusted Locations are in the `Exclude` list.) 1. Under `Access Controls`, select `Grant` select `Block Access`. 1. Set `Enable policy` to `Report-only`. 1. Click `Create`. Allow some time to pass to ensure the sign-in logs capture relevant conditional access events. These events will need to be reviewed to determine if additional considerations are necessary for your organization (e.g. legitimate locations are being blocked and investigation is needed for exception). **NOTE:** The policy is not yet 'live,' since `Report-only` is being used to audit the effect of the policy. Part 2 of 2 - Confirm that the policy is not blocking access that should be granted, then toggle to `On`. 1. With your policy now in report-only mode, return to the Microsoft Entra blade and click on `Sign-in logs`. 1. Review the recent sign-in events - click an event then review the event details (specifically the `Report-only` tab) to ensure: - The sign-in event you're reviewing occurred **after** turning on the policy in report-only mode - The policy name from step 6 above is listed in the `Policy Name` column - The `Result` column for the new policy shows that the policy was `Not applied` (indicating the location origin was not blocked) 1. If the above conditions are present, navigate back to the policy name in Conditional Access and open it. 1. Toggle the policy from `Report-only` to `On`. 1. Click `Save`. **Remediate from PowerShell** First, set up the conditions objects values before updating an existing conditional access policy or before creating a new one. You may need to use additional PowerShell cmdlets to retrieve specific IDs such as the `Get-MgIdentityConditionalAccessNamedLocation` which outputs the `Location IDs` for use with conditional access policies. ``` $conditions = New-Object -TypeName Microsoft.Open.MSGraph.Model.ConditionalAccessConditionSet $conditions.Applications = New-Object -TypeName Microsoft.Open.MSGraph.Model.ConditionalAccessApplicationCondition $conditions.Applications.IncludeApplications = $conditions.Applications.ExcludeApplications = $conditions.Users = New-Object -TypeName Microsoft.Open.MSGraph.Model.ConditionalAccessUserCondition $conditions.Users.IncludeUsers = $conditions.Users.ExcludeUsers = $conditions.Users.IncludeGroups = $conditions.Users.ExcludeGroups = $conditions.Users.IncludeRoles = $conditions.Users.ExcludeRoles = $conditions.Locations = New-Object -TypeName Microsoft.Open.MSGraph.Model.ConditionalAccessLocationCondition $conditions.Locations.IncludeLocations = $conditions.Locations.ExcludeLocations = $controls = New-Object -TypeName Microsoft.Open.MSGraph.Model.ConditionalAccessGrantControls $controls._Operator = OR $controls.BuiltInControls = block ``` Next, update the existing conditional access policy with the condition set options configured with the previous commands. ``` Update-MgIdentityConditionalAccessPolicy -PolicyId -Conditions $conditions -GrantControls $controls ``` To create a new conditional access policy that complies with this best practice, run the following commands after creating the condition set above ``` New-MgIdentityConditionalAccessPolicy -Name Policy Name -State -Conditions $conditions -GrantControls $controls ```",
+ "AuditProcedure": "**Audit from Azure Portal** 1. From Azure Home open the Portal menu in the top left, and select `Microsoft Entra ID`. 1. Scroll down in the menu on the left, and select `Security`. 1. Select on the left side `Conditional Access`. 1. Select `Policies`. 1. Select the policy you wish to audit, then: - Under `Assignments` > `Users`, review the users and groups for the personnel the policy will apply to - Under `Assignments` > `Target resources`, review the cloud apps or actions for the systems the policy will apply to - Under `Conditions` > `Locations`, Review the `Include` locations for those that should be **blocked** - Under `Conditions` > `Locations`, Review the `Exclude` locations for those that should be allowed (Note: locations set up in the previous recommendation for Trusted Location should be in the `Exclude` list.) - Under `Access Controls` > `Grant` - Confirm that `Block access` is selected. **Audit from Azure CLI** ``` As of this writing there are no subcommands for Conditional Access Policies within the Azure CLI ``` **Audit from PowerShell** ``` $conditionalAccessPolicies = Get-MgIdentityConditionalAccessPolicy foreach($policy in $conditionalAccessPolicies) {$policy | Select-Object @{N='Policy ID'; E={$policy.id}}, @{N=Included Locations; E={$policy.Conditions.Locations.IncludeLocations}}, @{N=Excluded Locations; E={$policy.Conditions.Locations.ExcludeLocations}}, @{N=BuiltIn GrantControls; E={$policy.GrantControls.BuiltInControls}}} ``` Make sure there is at least 1 row in the output of the above PowerShell command that contains `Block` under the `BuiltIn GrantControls` column and location IDs under the `Included Locations` and `Excluded Locations` columns. If not, a policy containing these options has not been created and is considered a finding.",
+ "AdditionalInformation": "These policies should be tested by using the What If tool in the References. Setting these can and will create issues with logging in for users until they use an MFA device linked to their accounts. Further testing can also be done via the insights and reporting resource in References which monitors Azure sign ins.",
+ "References": "https://docs.microsoft.com/en-us/azure/active-directory/conditional-access/howto-conditional-access-policy-location:https://docs.microsoft.com/en-us/azure/active-directory/conditional-access/concept-conditional-access-report-only:https://learn.microsoft.com/en-us/security/benchmark/azure/mcsb-identity-management#im-7-restrict-resource-access-based-on--conditions",
+ "DefaultValue": "This policy does not exist by default."
+ }
+ ]
+ },
+ {
+ "Id": "5.2.3",
+ "Description": "Ensure that an exclusionary device code flow policy is considered",
+ "Checks": [],
+ "Attributes": [
+ {
+ "Section": "5 Identity Services",
+ "SubSection": "5.2 Conditional Access",
+ "Profile": "Level 1",
+ "AssessmentStatus": "Manual",
+ "Description": "Conditional Access Policies can be used to prevent the Device code authentication flow. Device code flow should be permitted only for users that regularly perform duties that explicitly require the use of Device Code to authenticate, such as utilizing Azure with PowerShell.",
+ "RationaleStatement": "Attackers use Device code flow in phishing attacks and, if successful, results in the attacker gaining access tokens and refresh tokens which are scoped to user_impersonation, which can perform any action the user has permission to perform.",
+ "ImpactStatement": "Microsoft Entra ID P1 or P2 is required. This policy should be tested using the `Report-only mode` before implementation. Without a full and careful understanding of the accounts and personnel who require Device code authentication flow, implementing this policy can block authentication for users and devices who rely on Device code flow. For users and devices that rely on device code flow authentication, more secure alternatives should be implemented wherever possible.",
+ "RemediationProcedure": "**Remediate from Azure Portal** Part 1 of 2 - Create the policy and enable it in `Report-only` mode. 1. From Azure Home open the portal menu in the top left and select `Microsoft Entra ID`. 1. Scroll down in the menu on the left and select `Security`. 1. Select on the left side `Conditional Access`. 1. Select `Policies`. 1. Click the `+ New policy` button, then: 1. Provide a name for the policy. 1. Under `Assignments`, select `Users` then: - Under `Include`, select `All users` - Under `Exclude`, check Users and groups and only select emergency access accounts 1. Under `Assignments`, select `Target resources` then: - Under `Include`, select `All cloud apps` - Leave `Exclude` blank unless you have a well defined exception 1. Under `Conditions` > `Authentication Flows`, set Configure to `Yes` then: - Select `Device code flow` - Select `Done` 1. Under `Access Controls` > `Grant`, select `Block Access`. 1. Set `Enable policy` to `Report-only`. 1. Click `Create`. Allow some time to pass to ensure the sign-in logs capture relevant conditional access events. These events will need to be reviewed to determine if additional considerations are necessary for your organization (e.g. many legitimate use cases of device code authentication are observed). **NOTE:** The policy is not yet 'live,' since `Report-only` is being used to audit the effect of the policy. Part 2 of 2 - Confirm that the policy is not blocking access that should be granted, then toggle to `On`. 1. With your policy now in report-only mode, return to the Microsoft Entra blade and click on `Sign-in logs`. 1. Review the recent sign-in events - click an event then review the event details (specifically the `Report-only` tab) to ensure: - The sign-in event you're reviewing occurred **after** turning on the policy in report-only mode - The policy name from step 6 above is listed in the `Policy Name` column - The `Result` column for the new policy shows that the policy was `Not applied` (indicating the device code authentication flow was not blocked) 1. If the above conditions are present, navigate back to the policy name in Conditional Access and open it. 1. Toggle the policy from `Report-only` to `On`. 1. Click `Save`.",
+ "AuditProcedure": "**Audit from Azure Portal** 1. From Azure Home open the Portal menu in the top left and select `Microsoft Entra ID`. 1. Scroll down in the menu on the left and select `Security`. 1. Select on the left side `Conditional Access`. 1. Select `Policies`. 1. Select the policy you wish to audit, then: - Under `Assignments` > `Users`, review the users and groups for the personnel the policy will apply to - Under `Assignments` > `Target resources`, review the cloud apps or actions for the systems the policy will apply to - Under `Conditions` > `Authentication Flows`, review the configuration to ensure `Device code flow` is selected - Under `Access Controls` > `Grant` - Confirm that `Block access` is selected.",
+ "AdditionalInformation": "These policies should be tested by using the What If tool in the References. Setting these can and will create issues with logging in for users until they use an MFA device linked to their accounts. Further testing can also be done via the insights and reporting resource in References which monitors Azure sign ins.",
+ "References": "https://learn.microsoft.com/en-us/entra/identity/conditional-access/concept-authentication-flows#device-code-flow:https://learn.microsoft.com/en-us/security/benchmark/azure/mcsb-identity-management#im-7-restrict-resource-access-based-on--conditions:https://docs.microsoft.com/en-us/azure/active-directory/conditional-access/concept-conditional-access-report-only:https://learn.microsoft.com/en-us/entra/identity/conditional-access/how-to-policy-authentication-flows",
+ "DefaultValue": "This policy does not exist by default."
+ }
+ ]
+ },
+ {
+ "Id": "5.2.4",
+ "Description": "Ensure that a multifactor authentication policy exists for all users",
+ "Checks": [
+ "entra_non_privileged_user_has_mfa",
+ "entra_privileged_user_has_mfa"
+ ],
+ "Attributes": [
+ {
+ "Section": "5 Identity Services",
+ "SubSection": "5.2 Conditional Access",
+ "Profile": "Level 1",
+ "AssessmentStatus": "Manual",
+ "Description": "A Conditional Access policy can be enabled to ensure that users are required to use Multifactor Authentication (MFA) to login. **Note:** Since 2024, Azure has been rolling out mandatory multifactor authentication. For more information: - https://azure.microsoft.com/en-us/blog/announcing-mandatory-multi-factor-authentication-for-azure-sign-in - https://learn.microsoft.com/en-us/entra/identity/authentication/concept-mandatory-multifactor-authentication",
+ "RationaleStatement": "Multifactor authentication is strongly recommended to increase the confidence that a claimed identity can be proven to be the subject of the identity. This results in a stronger authentication chain and reduced likelihood of exploitation.",
+ "ImpactStatement": "There is an increased cost associated with Conditional Access policies because of the requirement of Microsoft Entra ID P1 or P2 licenses. Additional support overhead may also need to be considered.",
+ "RemediationProcedure": "**Remediate from Azure Portal** 1. From Azure Home open Portal menu in the top left, and select `Microsoft Entra ID`. 1. Select `Security`. 1. Select `Conditional Access`. 1. Select `Policies`. 1. Click `+ New policy`. 1. Enter a name for the policy. 1. Click the blue text under `Users`. 1. Under `Include`, select `All users`. 1. Under `Exclude`, check `Users and groups`. 1. Select users this policy should not apply to and click `Select`. 1. Click the blue text under `Target resources`. 1. Select `All cloud apps`. 1. Click the blue text under `Grant`. 1. Under `Grant access`, check `Require multifactor authentication` and click `Select`. 1. Set `Enable policy` to `Report-only`. 1. Click `Create`. After testing the policy in report-only mode, update the `Enable policy` setting from `Report-only` to `On`.",
+ "AuditProcedure": "**Audit from Azure Portal** 1. From Azure Home open the Portal Menu in the top left, and select `Microsoft Entra ID`. 1. Scroll down in the menu on the left, and select `Security`. 1. Select on the left side `Conditional Access`. 1. Select `Policies`. 1. Select the policy you wish to audit. 1. Click the blue text under `Users`. 1. Under `Include` ensure that `All Users` is specified. 1. Under `Exclude` ensure that no users or groups are specified. If there are users or groups specified for exclusion, a very strong justification should exist for each exception, and all excepted account-level objects should be recorded in documentation along with the justification for comparison in future audits.",
+ "AdditionalInformation": "These policies should be tested by using the What If tool in the References. Setting these can and will create issues with logging in for users until they use an MFA device linked to their accounts. Further testing can also be done via the insights and reporting resource in the References which monitors Azure sign ins.",
+ "References": "https://docs.microsoft.com/en-us/azure/active-directory/conditional-access/howto-conditional-access-policy-all-users-mfa:https://docs.microsoft.com/en-us/azure/active-directory/conditional-access/troubleshoot-conditional-access-what-if:https://docs.microsoft.com/en-us/azure/active-directory/conditional-access/howto-conditional-access-insights-reporting:https://learn.microsoft.com/en-us/security/benchmark/azure/mcsb-identity-management#im-7-restrict-resource-access-based-on--conditions",
+ "DefaultValue": "Starting October 2024, MFA will be required for all accounts by default."
+ }
+ ]
+ },
+ {
+ "Id": "5.2.5",
+ "Description": "Ensure that multifactor authentication is required for risky sign-ins",
+ "Checks": [],
+ "Attributes": [
+ {
+ "Section": "5 Identity Services",
+ "SubSection": "5.2 Conditional Access",
+ "Profile": "Level 1",
+ "AssessmentStatus": "Manual",
+ "Description": "Entra ID tracks the behavior of sign-in events. If the Entra ID domain is licensed with P2, the sign-in behavior can be used as a detection mechanism for additional scrutiny during the sign-in event. If this policy is set up, then Risky Sign-in events will prompt users to use multi-factor authentication (MFA) tokens on login for additional verification.",
+ "RationaleStatement": "Enabling multi-factor authentication is a recommended setting to limit the potential of accounts being compromised and limiting access to authenticated personnel. Enabling this policy allows Entra ID's risk-detection mechanisms to force additional scrutiny on the login event, providing a deterrent response to potentially malicious sign-in events, and adding an additional authentication layer as a reaction to potentially malicious behavior.",
+ "ImpactStatement": "Risk Policies for Conditional Access require Microsoft Entra ID P2. Additional overhead to support or maintain these policies may also be required if users lose access to their MFA tokens.",
+ "RemediationProcedure": "**Remediate from Azure Portal** 1. From Azure Home select the Portal Menu in the top left and select `Microsoft Entra ID`. 1. Select `Security` 1. Select `Conditional Access`. 1. Select `Policies`. 1. Click `+ New policy`. 1. Enter a name for the policy. 1. Click the blue text under `Users`. 1. Under `Include`, select `All users`. 1. Under `Exclude`, check `Users and groups`. 1. Select users this policy should not apply to and click `Select`. 1. Click the blue text under `Target resources`. 1. Select `All cloud apps`. 1. Click the blue text under `Conditions`. 1. Select `Sign-in risk`. 1. Update the `Configure` toggle to `Yes`. 1. Check the sign-in risk level this policy should apply to, e.g. `High` and `Medium`. 1. Select `Done`. 1. Click the blue text under `Grant` and check `Require multifactor authentication` then click the `Select` button. 1. Click the blue text under `Session` then check `Sign-in frequency` and select `Every time` and click the `Select` button. 1. Set `Enable policy` to `Report-only`. 1. Click `Create`. After testing the policy in report-only mode, update the `Enable policy` setting from `Report-only` to `On`.",
+ "AuditProcedure": "**Audit from Azure Portal** 1. From Azure Home select the Portal Menu in the top left and select `Microsoft Entra ID`. 1. Select `Security`. 1. Select on the left side `Conditional Access`. 1. Select `Policies`. 1. Select the policy you wish to audit. 1. Click the blue text under `Users`. 1. View under `Include` the corresponding users and groups to whom the policy is applied. 1. View under `Exclude` to determine which users and groups to whom the policy is not applied.",
+ "AdditionalInformation": "These policies should be tested by using the What If tool in the References. Setting these can and will create issues with logging in for users until they use an MFA device linked to their accounts. Further testing can also be done via the insights and reporting resource the in References which monitors Azure sign ins.",
+ "References": "https://learn.microsoft.com/en-us/entra/identity/conditional-access/howto-conditional-access-policy-risk:https://learn.microsoft.com/en-us/entra/identity/conditional-access/troubleshoot-conditional-access-what-if:https://learn.microsoft.com/en-us/entra/identity/conditional-access/howto-conditional-access-insights-reporting:https://learn.microsoft.com/en-us/security/benchmark/azure/mcsb-identity-management#im-7-restrict-resource-access-based-on--conditions:https://learn.microsoft.com/en-us/entra/id-protection/overview-identity-protection#license-requirements",
+ "DefaultValue": "MFA is not enabled by default."
+ }
+ ]
+ },
+ {
+ "Id": "5.2.6",
+ "Description": "Ensure that multifactor authentication is required for Windows Azure Service Management API",
+ "Checks": [
+ "entra_conditional_access_policy_require_mfa_for_management_api"
+ ],
+ "Attributes": [
+ {
+ "Section": "5 Identity Services",
+ "SubSection": "5.2 Conditional Access",
+ "Profile": "Level 1",
+ "AssessmentStatus": "Manual",
+ "Description": "This recommendation ensures that users accessing the Windows Azure Service Management API (i.e. Azure Powershell, Azure CLI, Azure Resource Manager API, etc.) are required to use multi-factor authentication (MFA) credentials when accessing resources through the Windows Azure Service Management API.",
+ "RationaleStatement": "Administrative access to the Windows Azure Service Management API should be secured with a higher level of scrutiny to authenticating mechanisms. Enabling multi-factor authentication is recommended to reduce the potential for abuse of Administrative actions, and to prevent intruders or compromised admin credentials from changing administrative settings. **IMPORTANT**: While this recommendation allows exceptions to specific Users or Groups, they should be very carefully tracked and reviewed for necessity on a regular interval through an Access Review process. It is important that this rule be built to include All Users to ensure that all users not specifically excepted will be required to use MFA to access the Azure Service Management API.",
+ "ImpactStatement": "Conditional Access policies require Microsoft Entra ID P1 or P2 licenses. Similarly, they may require additional overhead to maintain if users lose access to their MFA. Any users or groups which are granted an exception to this policy should be carefully tracked, be granted only minimal necessary privileges, and conditional access exceptions should be regularly reviewed or investigated.",
+ "RemediationProcedure": "**Remediate from Azure Portal** 1. From the Azure Admin Portal dashboard, open `Microsoft Entra ID`. 1. Click `Security` in the Entra ID blade. 1. Click `Conditional Access` in the Security blade. 1. Click `Policies` in the Conditional Access blade. 1. Click `+ New policy`. 1. Enter a name for the policy. 1. Click the blue text under `Users`. 1. Under `Include`, select `All users`. 1. Under `Exclude`, check `Users and groups`. 1. Select users or groups to be exempted from this policy (e.g. break-glass emergency accounts, and non-interactive service accounts) then click the `Select` button. 1. Click the blue text under `Target resources`. 1. Under `Include`, click the `Select apps` radio button. 1. Click the blue text under `Select`. 1. Check the box next to `Windows Azure Service Management APIs` then click the `Select` button. 1. Click the blue text under `Grant`. 1. Under `Grant access` check the box for `Require multi-factor authentication` then click the `Select` button. 1. Before creating, set `Enable policy` to `Report-only`. 1. Click `Create`. After testing the policy in report-only mode, update the `Enable policy` setting from `Report-only` to `On`.",
+ "AuditProcedure": "**Audit from Azure Portal** 1. From the Azure Admin Portal dashboard, open `Microsoft Entra ID`. 1. In the menu on the left of the Entra ID blade, click `Security`. 1. In the menu on the left of the Security blade, click `Conditional Access`. 1. In the menu on the left of the Conditional Access blade, click `Policies`. 1. Click on the name of the policy you wish to audit. 1. Click the blue text under `Users`. 1. Under the `Include` section of Users, ensure that `All Users` is selected. 1. Under the `Exclude` section of Users, review the `Users and Groups` that are excluded from the policy (NOTE: this should be limited to break-glass emergency access accounts, non-interactive service accounts, and other carefully considered exceptions). 1. On the left side, click the blue text under `Target resources`. 1. Under the `Include` section of Target Resources, ensure that the `Select apps` radio button is selected. 1. Under `Select`, ensure that `Windows Azure Service Management API` is listed.",
+ "AdditionalInformation": "These policies should be tested by using the What If tool in the References. Setting these can and will create issues with administrators changing settings until they use an MFA device linked to their accounts. An emergency access account is recommended for this eventuality if all administrators are locked out. Please see the documentation in the references for further information. Similarly further testing can also be done via the insights and reporting resource in References which monitors Azure sign ins.",
+ "References": "https://learn.microsoft.com/en-us/security/benchmark/azure/mcsb-identity-management#im-7-restrict-resource-access-based-on--conditions:https://docs.microsoft.com/en-us/azure/active-directory/conditional-access/concept-conditional-access-users-groups:https://learn.microsoft.com/en-us/entra/identity/conditional-access/howto-conditional-access-policy-azure-management:https://learn.microsoft.com/en-us/entra/identity/conditional-access/concept-conditional-access-cloud-apps#windows-azure-service-management-api",
+ "DefaultValue": "MFA is not enabled by default for administrative actions."
+ }
+ ]
+ },
+ {
+ "Id": "5.2.7",
+ "Description": "Ensure that multifactor authentication is required to access Microsoft Admin Portals",
+ "Checks": [
+ "defender_ensure_defender_for_server_is_on"
+ ],
+ "Attributes": [
+ {
+ "Section": "5 Identity Services",
+ "SubSection": "5.2 Conditional Access",
+ "Profile": "Level 1",
+ "AssessmentStatus": "Manual",
+ "Description": "This recommendation ensures that users accessing Microsoft Admin Portals (i.e. Microsoft 365 Admin, Microsoft 365 Defender, Exchange Admin Center, Azure Portal, etc.) are required to use multi-factor authentication (MFA) credentials when logging into an Admin Portal.",
+ "RationaleStatement": "Administrative Portals for Microsoft Azure should be secured with a higher level of scrutiny to authenticating mechanisms. Enabling multi-factor authentication is recommended to reduce the potential for abuse of Administrative actions, and to prevent intruders or compromised admin credentials from changing administrative settings. **IMPORTANT**: While this recommendation allows exceptions to specific Users or Groups, they should be very carefully tracked and reviewed for necessity on a regular interval through an Access Review process. It is important that this rule be built to include All Users to ensure that all users not specifically excepted will be required to use MFA to access Admin Portals.",
+ "ImpactStatement": "Conditional Access policies require Microsoft Entra ID P1 or P2 licenses. Similarly, they may require additional overhead to maintain if users lose access to their MFA. Any users or groups which are granted an exception to this policy should be carefully tracked, be granted only minimal necessary privileges, and conditional access exceptions should be reviewed or investigated.",
+ "RemediationProcedure": "**Remediate from Azure Portal** 1. From the Azure Admin Portal dashboard, open `Microsoft Entra ID`. 1. Click `Security` in the Entra ID blade. 1. Click `Conditional Access` in the Security blade. 1. Click `Policies` in the Conditional Access blade. 1. Click `+ New policy`. 1. Enter a name for the policy. 1. Click the blue text under `Users`. 1. Under `Include`, select `All users`. 1. Under `Exclude`, check `Users and groups`. 1. Select users or groups to be exempted from this policy (e.g. break-glass emergency accounts, and non-interactive service accounts) then click the `Select` button. 1. Click the blue text under `Target resources`. 1. Under `Include`, click the `Select apps` radio button. 1. Click the blue text under `Select`. 1. Check the box next to `Microsoft Admin Portals` then click the `Select` button. 1. Click the blue text under `Grant`. 1. Under `Grant access` check the box for `Require multifactor authentication` then click the `Select` button. 1. Before creating, set `Enable policy` to `Report-only`. 1. Click `Create`. After testing the policy in report-only mode, update the `Enable policy` setting from `Report-only` to `On`.",
+ "AuditProcedure": "**Audit from Azure Portal** 1. From the Azure Admin Portal dashboard, open `Microsoft Entra ID`. 1. In the menu on the left of the Entra ID blade, click `Security`. 1. In the menu on the left of the Security blade, click `Conditional Access`. 1. In the menu on the left of the Conditional Access blade, click `Policies`. 1. Click on the name of the policy you wish to audit. 1. Click the blue text under `Users`. 1. Under the `Include` section of Users, review `Users and Groups` to ensure that `All Users` is selected. 1. Under the `Exclude` section of Users, review the `Users and Groups` that are excluded from the policy (NOTE: this should be limited to break-glass emergency access accounts, non-interactive service accounts, and other carefully considered exceptions). 1. On the left side, click the blue text under `Target Resources`. 1. Under the `Include` section of Target resources, ensure the `Select apps` radio button is selected. 1. Under `Select`, ensure `Microsoft Admin Portals` is listed.",
+ "AdditionalInformation": "These policies should be tested by using the What If tool in the References. Setting these can and will create issues with administrators changing settings until they use an MFA device linked to their accounts. An emergency access account is recommended for this eventuality if all administrators are locked out. Please see the documentation in the references for further information. Similarly further testing can also be done via the insights and reporting resource in References which monitors Azure sign ins.",
+ "References": "https://learn.microsoft.com/en-us/security/benchmark/azure/mcsb-identity-management#im-7-restrict-resource-access-based-on--conditions:https://docs.microsoft.com/en-us/azure/active-directory/conditional-access/concept-conditional-access-users-groups:https://learn.microsoft.com/en-us/entra/identity/conditional-access/how-to-policy-mfa-admin-portals",
+ "DefaultValue": "MFA is not enabled by default for administrative actions."
+ }
+ ]
+ },
+ {
+ "Id": "5.2.8",
+ "Description": "Ensure a Token Protection Conditional Access policy is considered",
+ "Checks": [],
+ "Attributes": [
+ {
+ "Section": "5 Identity Services",
+ "SubSection": "5.2 Conditional Access",
+ "Profile": "Level 2",
+ "AssessmentStatus": "Manual",
+ "Description": "This recommendation ensures that issued tokens are only issued to the intended device.",
+ "RationaleStatement": "When properly configured, conditional access can aid in preventing attacks involving token theft, via hijacking or reply, as part of the attack flow. Although currently considered a rare event, the impact from token impersonation can be severe.",
+ "ImpactStatement": "A Microsoft Entra ID P1 or P2 license is required. Start with a Conditional Access policy in 'Report Only' mode prior to enforcing for all users.",
+ "RemediationProcedure": "**Remediate from Azure Portal** 1. Sign in to the Microsoft Entra admin center as at least a Conditional Access Administrator. 2. Browse to `Protection` > `Conditional Access` > `Policies`. 3. Select `New policy`. 4. Give your policy a name. 5. Under `Assignments`, select `Users or workload identities` and configure scope. 6. Under `Target resources` > `Resources` > `Include` > `Select resources`, select `Office 365 Exchange Online` and `Office 365 SharePoint Online`. 7. Under `Conditions` > `Device platforms`, set `Configure` to `Yes` and select `Windows`. 8. Under `Conditions` > `Client apps`, set `Configure` to `Yes` and select `Mobile apps and desktop clients`. 9. Under `Access controls` > `Session`, select `Require token protection for sign-in sessions`. 10. Confirm settings and set `Enable policy` to `On`. 11. Click `Create`.",
+ "AuditProcedure": "**Audit from Azure Portal** 1. Sign in to the Microsoft Entra admin center as at least a Conditional Access Administrator. 2. Browse to `Protection` > `Conditional Access` > `Policies`. 3. Review existing policies to ensure that at least one policy contains the following configuration: 4. Under `Assignments`, review `Users or workload identities` and ensure the scope is appropriate. 5. Under `Target resources` > `Resources` > `Include` > `Select resources`: Ensure that both `Office 365 Exchange Online` and `Office 365 SharePoint Online` are selected. 6. Under `Conditions` > `Device Platforms`: Ensure `Configure` is set to `Yes` and `Include` indicates Windows platforms. 7. Under `Conditions` > `Client Apps`: Ensure `Configure` is set to `Yes` and `Mobile Apps and Desktop Clients` is selected. 8. Under `Access controls` > `Session`, ensure that `Require token protection for sign-in sessions` is selected.",
+ "AdditionalInformation": "",
+ "References": "https://learn.microsoft.com/en-us/entra/identity/conditional-access/concept-token-protection:https://www.microsoft.com/en-gb/security/business/microsoft-entra-pricing",
+ "DefaultValue": "A Token Protection Conditional Access policy does not exist by default."
+ }
+ ]
+ },
+ {
+ "Id": "5.3.1",
+ "Description": "Ensure that Azure admin accounts are not used for daily operations",
+ "Checks": [],
+ "Attributes": [
+ {
+ "Section": "5 Identity Services",
+ "SubSection": "5.3 Periodic Identity Reviews",
+ "Profile": "Level 1",
+ "AssessmentStatus": "Manual",
+ "Description": "Microsoft Azure admin accounts should not be used for routine, non-administrative tasks.",
+ "RationaleStatement": "Using admin accounts for daily operations increases the risk of accidental misconfigurations and security breaches.",
+ "ImpactStatement": "Minor administrative overhead includes managing separate accounts, enforcing stricter access controls, and potential licensing costs for advanced security features.",
+ "RemediationProcedure": "If admin accounts are being used for daily operations, consider the following: - Monitor and alert on unusual activity. - Enforce the principle of least privilege. - Revoke any unnecessary administrative access. - Use Conditional Access to limit access to resources. - Ensure that administrators have separate admin and user accounts. - Use Microsoft Entra ID Protection helps organizations detect, investigate, and remediate identity-based risks. - Use Privileged Identity Management (PIM) in Microsoft Entra ID to limit standing administrator access to privileged roles, discover who has access, and review privileged access.",
+ "AuditProcedure": "**Audit from Azure Portal** Monitor: 1. Go to `Monitor`. 1. Click `Activity log`. 1. Review the activity log and ensure that admin accounts are not being used for daily operations. Microsoft Entra ID: 1. Go to `Microsoft Entra ID`. 1. Under `Monitoring`, click `Sign-in logs`. 1. Review the sign-in logs and ensure that admin accounts are not being accessed more frequently than necessary.",
+ "AdditionalInformation": "",
+ "References": "https://learn.microsoft.com/en-us/security/privileged-access-workstations/critical-impact-accounts",
+ "DefaultValue": ""
+ }
+ ]
+ },
+ {
+ "Id": "5.3.2",
+ "Description": "Ensure that guest users are reviewed on a regular basis",
+ "Checks": [
+ "entra_policy_guest_users_access_restrictions"
+ ],
+ "Attributes": [
+ {
+ "Section": "5 Identity Services",
+ "SubSection": "5.3 Periodic Identity Reviews",
+ "Profile": "Level 1",
+ "AssessmentStatus": "Manual",
+ "Description": "Microsoft Entra ID has native and extended identity functionality allowing you to invite people from outside your organization to be guest users in your cloud account and sign in with their own work, school, or social identities.",
+ "RationaleStatement": "Guest users are typically added outside your employee on-boarding/off-boarding process and could potentially be overlooked indefinitely. To prevent this, guest users should be reviewed on a regular basis. During this audit, guest users should also be determined to not have administrative privileges.",
+ "ImpactStatement": "Before removing guest users, determine their use and scope. Like removing any user, there may be unforeseen consequences to systems if an account is removed without careful consideration.",
+ "RemediationProcedure": "**Remediate from Azure Portal** 1. From Azure Home select the Portal Menu 1. Select `Microsoft Entra ID` 1. Under `Manage`, select `Users` 1. Click on `Add filter` 1. Select `User type` 1. Select `Guest` from the Value dropdown 1. Click `Apply` 1. Check the box next to all `Guest` users that are no longer required or are inactive 1. Click `Delete` 1. Click `OK` **Remediate from Azure CLI** Before deleting the user, set it to inactive using the ID from the Audit Procedure to determine if there are any dependent systems. ``` az ad user update --id --account-enabled {false} ``` After determining that there are no dependent systems, delete the user. ``` Remove-AzureADUser -ObjectId ``` **Remediate from Azure PowerShell** Before deleting the user, set it to inactive using the ID from the Audit Procedure to determine if there are any dependent systems. ``` Set-AzureADUser -ObjectId -AccountEnabled false ``` After determining that there are no dependent systems, delete the user. ``` PS C:\\>Remove-AzureADUser -ObjectId ```",
+ "AuditProcedure": "**Audit from Azure Portal** 1. From Azure Home select the Portal Menu 1. Select `Microsoft Entra ID` 1. Under `Manage`, select `Users` 1. Click on `Add filter` 1. Select `User type` 1. Select `Guest` from the Value dropdown 1. Click `Apply` 1. Audit the listed guest users **Audit from Azure CLI** ``` az ad user list --query [?userType=='Guest'] ``` Ensure all users listed are still required and not inactive. **Audit from Azure PowerShell** ``` Get-AzureADUser |Where-Object {$_.UserType -like Guest} |Select-Object DisplayName, UserPrincipalName, UserType -Unique ``` **Audit from Azure Policy** If referencing a digital copy of this Benchmark, clicking a Policy ID will open a link to the associated Policy definition in Azure. If referencing a printed copy, you can search Policy IDs from this URL: https://portal.azure.com/#view/Microsoft_Azure_Policy/PolicyMenuBlade/~/Definitions - **Policy ID:** [e9ac8f8e-ce22-4355-8f04-99b911d6be52](https://portal.azure.com/#view/Microsoft_Azure_Policy/PolicyDetailBlade/definitionId/%2Fproviders%2FMicrosoft.Authorization%2FpolicyDefinitions%2Fe9ac8f8e-ce22-4355-8f04-99b911d6be52) **- Name:** 'Guest accounts with read permissions on Azure resources should be removed' - **Policy ID:** [94e1c2ac-cbbe-4cac-a2b5-389c812dee87](https://portal.azure.com/#view/Microsoft_Azure_Policy/PolicyDetailBlade/definitionId/%2Fproviders%2FMicrosoft.Authorization%2FpolicyDefinitions%2F94e1c2ac-cbbe-4cac-a2b5-389c812dee87) **- Name:** 'Guest accounts with write permissions on Azure resources should be removed' - **Policy ID:** [339353f6-2387-4a45-abe4-7f529d121046](https://portal.azure.com/#view/Microsoft_Azure_Policy/PolicyDetailBlade/definitionId/%2Fproviders%2FMicrosoft.Authorization%2FpolicyDefinitions%2F339353f6-2387-4a45-abe4-7f529d121046) **- Name:** 'Guest accounts with owner permissions on Azure resources should be removed'",
+ "AdditionalInformation": "It is good practice to use a dynamic security group to manage guest users. To create the dynamic security group: 1. Navigate to the 'Microsoft Entra ID' blade in the Azure Portal 2. Select the 'Groups' item 3. Create new 4. Type of 'dynamic' 5. Use the following dynamic selection rule. (user.userType -eq Guest) 6. Once the group has been created, select access reviews option and create a new access review with a period of monthly and send to relevant administrators for review.",
+ "References": "https://learn.microsoft.com/en-us/entra/external-id/user-properties:https://learn.microsoft.com/en-us/entra/fundamentals/how-to-create-delete-users#delete-a-user:https://learn.microsoft.com/en-us/security/benchmark/azure/mcsb-privileged-access#pa-4-review-and-reconcile-user-access-regularly:https://www.microsoft.com/en-us/security/business/identity-access-management/azure-ad-pricing:https://learn.microsoft.com/en-us/entra/identity/monitoring-health/howto-manage-inactive-user-accounts:https://learn.microsoft.com/en-us/entra/fundamentals/users-restore",
+ "DefaultValue": "By default no guest users are created."
+ }
+ ]
+ },
+ {
+ "Id": "5.3.3",
+ "Description": "Ensure that use of the 'User Access Administrator' role is restricted",
+ "Checks": [
+ "iam_role_user_access_admin_restricted"
+ ],
+ "Attributes": [
+ {
+ "Section": "5 Identity Services",
+ "SubSection": "5.3 Periodic Identity Reviews",
+ "Profile": "Level 1",
+ "AssessmentStatus": "Automated",
+ "Description": "The User Access Administrator role grants the ability to view all resources and manage access assignments at any subscription or management group level within the tenant. Due to its high privilege level, this role assignment should be removed immediately after completing the necessary changes at the root scope to minimize security risks.",
+ "RationaleStatement": "The User Access Administrator role provides extensive access control privileges. Unnecessary assignments heighten the risk of privilege escalation and unauthorized access. Removing the role immediately after use minimizes security exposure.",
+ "ImpactStatement": "Increased administrative effort to manage and remove role assignments appropriately.",
+ "RemediationProcedure": "**Remediate from Azure Portal** 1. From Azure Home select the Portal Menu. 1. Select `Subscriptions`. 1. Select a subscription. 1. Select `Access control (IAM)`. 1. Look for the following banner at the top of the page: `Action required: X users have elevated access in your tenant. You should take immediate action and remove all role assignments with elevated access.` 1. Click `View role assignments`. 1. Click `Remove`. **Remediate from Azure CLI** Run the following command: ``` az role assignment delete --role User Access Administrator --scope / ```",
+ "AuditProcedure": "**Audit from Azure Portal** 1. From Azure Home select the Portal Menu. 1. Select `Subscriptions`. 1. Select a subscription. 1. Select `Access control (IAM)`. 1. Look for the following banner at the top of the page: `Action required: X users have elevated access in your tenant. You should take immediate action and remove all role assignments with elevated access.` If the banner is displayed, the `User Access Administrator` is assigned. **Audit from Azure CLI** Run the following command: ``` az role assignment list --role User Access Administrator --scope / ``` Ensure that the command does not return any `User Access Administrator` role assignment information.",
+ "AdditionalInformation": "",
+ "References": "https://learn.microsoft.com/en-us/azure/role-based-access-control/built-in-roles:https://learn.microsoft.com/en-us/azure/role-based-access-control/elevate-access-global-admin",
+ "DefaultValue": ""
+ }
+ ]
+ },
+ {
+ "Id": "5.3.4",
+ "Description": "Ensure that all 'privileged' role assignments are periodically reviewed",
+ "Checks": [],
+ "Attributes": [
+ {
+ "Section": "5 Identity Services",
+ "SubSection": "5.3 Periodic Identity Reviews",
+ "Profile": "Level 1",
+ "AssessmentStatus": "Manual",
+ "Description": "Periodic review of privileged role assignments is performed to ensure that the privileged roles assigned to users are accurate and appropriate.",
+ "RationaleStatement": "Privileged roles are crown jewel assets that can be used by malicious insiders, threat actors, and even through mistake to significantly damage an organization. These roles should be periodically reviewed to identify lingering permissions assignment and detect lateral movement through privilege escalation.",
+ "ImpactStatement": "Increased administrative effort to manage and remove role assignments appropriately.",
+ "RemediationProcedure": "Review privileged role assignments and remove any that are no longer necessary or appropriate. Use Azure PIM (Privileged Identity Management) to implement just-in-time access for privileged roles.",
+ "AuditProcedure": "**Audit from Azure Portal** 1. From Azure Home select the Portal Menu. 2. Select `Subscriptions`. 3. Select a subscription. 4. Select `Access control (IAM)`. 5. Look for the number under the word `Privileged` accompanied by a link titled `View Assignments`. Click the `View assignments` link. 6. For each privileged role listed, evaluate whether the assignment is appropriate and current for each User, Group, or App assigned to each privileged role. NOTE: Determining 'appropriate' assignments requires a clear understanding of your organization's personnel, systems, policy, and security requirements.",
+ "AdditionalInformation": "",
+ "References": "https://learn.microsoft.com/en-us/azure/role-based-access-control/built-in-roles",
+ "DefaultValue": ""
+ }
+ ]
+ },
+ {
+ "Id": "5.3.5",
+ "Description": "Ensure disabled user accounts do not have read, write, or owner permissions",
+ "Checks": [],
+ "Attributes": [
+ {
+ "Section": "5 Identity Services",
+ "SubSection": "5.3 Periodic Identity Reviews",
+ "Profile": "Level 1",
+ "AssessmentStatus": "Manual",
+ "Description": "Ensure that any roles granting read, write, or owner permissions are removed from disabled Azure user accounts.",
+ "RationaleStatement": "Disabled accounts should not retain access to resources, as this poses a security risk. Removing role assignments mitigates potential unauthorized access and enforces the principle of least privilege.",
+ "ImpactStatement": "Ensure disabled accounts are not relied on for break glass or automated processes before removing roles to avoid service disruption.",
+ "RemediationProcedure": "**Remediate from Azure Portal** 1. Go to `Microsoft Entra ID`. 2. Under `Manage`, click `Users`. 3. Click `Add filter`. 4. Click `Account enabled`. 5. Click the toggle switch to set the value to `No`. 6. Click `Apply`. 7. Click the `Display name` of a disabled user account with read, write, or owner roles assigned. 8. Click `Azure role assignments`. 9. Click the name of a read, write, or owner role. 10. Click `Assignments`. 11. Click `Remove` in the row for the disabled user account. 12. Click `Yes`. 13. Repeat steps 7-12 for disabled user accounts requiring remediation. **Remediate from PowerShell** ``` Remove-AzRoleAssignment -ObjectId $user.ObjectId -RoleDefinitionName ```",
+ "AuditProcedure": "**Audit from Azure Portal** 1. Go to `Microsoft Entra ID`. 2. Under `Manage`, click `Users`. 3. Click `Add filter`. 4. Click `Account enabled`. 5. Click the toggle switch to set the value to `No`. 6. Click `Apply`. 7. Click the `Display name` of a disabled user account. 8. Click `Azure role assignments`. 9. Ensure that no read, write, or owner roles are assigned to the user account. 10. Repeat steps 7-9 for each disabled user account. **Audit from PowerShell** ``` Connect-AzureAD Get-AzureADUser $user = Get-AzureADUser -ObjectId $user.AccountEnabled ``` If AccountEnabled is False, run: ``` Get-AzRoleAssignment -ObjectId $user.ObjectId ``` **Audit from Azure Policy** - **Policy ID:** [0cfea604-3201-4e14-88fc-fae4c427a6c5] - Name: 'Blocked accounts with owner permissions on Azure resources should be removed' - **Policy ID:** [8d7e1fde-fe26-4b5f-8108-f8e432cbc2be] - Name: 'Blocked accounts with read and write permissions on Azure resources should be removed'",
+ "AdditionalInformation": "",
+ "References": "https://learn.microsoft.com/en-us/powershell/module/az.resources/get-azaduser:https://learn.microsoft.com/en-us/powershell/module/az.resources/get-azroleassignment:https://learn.microsoft.com/en-us/powershell/module/az.resources/remove-azroleassignment",
+ "DefaultValue": "Disabled user accounts retain their prior role assignments."
+ }
+ ]
+ },
+ {
+ "Id": "5.3.6",
+ "Description": "Ensure 'Tenant Creator' role assignments are periodically reviewed",
+ "Checks": [],
+ "Attributes": [
+ {
+ "Section": "5 Identity Services",
+ "SubSection": "5.3 Periodic Identity Reviews",
+ "Profile": "Level 1",
+ "AssessmentStatus": "Manual",
+ "Description": "Perform a periodic review of the Tenant Creator role assignment to ensure that the assignments are accurate and appropriate.",
+ "RationaleStatement": "Unnecessary assignments increase the risk of privilege escalation and unauthorized access. This recommendation should be applied alongside the recommendation 'Ensure that Restrict non-admin users from creating tenants is set to Yes'.",
+ "ImpactStatement": "Verify that the Tenant Creator role is no longer required by any assignments before removal to avoid disruption of critical functions.",
+ "RemediationProcedure": "**Remediate from Azure Portal** 1. Go to `Microsoft Entra ID`. 2. Under `Manage`, click `Roles and administrators`. 3. In the search bar, type `Tenant Creator`. 4. Click the role. 5. Click the name of an assignment. 6. Check the box next to the `Tenant Creator` role. 7. Click `X Remove assignments`. 8. Click `Yes`. 9. Repeat steps 1-8 for each assignment requiring remediation.",
+ "AuditProcedure": "**Audit from Azure Portal** 1. Go to `Microsoft Entra ID`. 2. Under `Manage`, click `Roles and administrators`. 3. In the search bar, type `Tenant Creator`. 4. Click the role. 5. Review the assignments and ensure that they are appropriate.",
+ "AdditionalInformation": "",
+ "References": "https://learn.microsoft.com/en-us/azure/active-directory-b2c/tenant-management-check-tenant-creation-permission:https://learn.microsoft.com/en-us/entra/identity/role-based-access-control/permissions-reference#tenant-creator",
+ "DefaultValue": "The Tenant Creator role is not assigned by default."
+ }
+ ]
+ },
+ {
+ "Id": "5.3.7",
+ "Description": "Ensure all non-privileged role assignments are periodically reviewed",
+ "Checks": [],
+ "Attributes": [
+ {
+ "Section": "5 Identity Services",
+ "SubSection": "5.3 Periodic Identity Reviews",
+ "Profile": "Level 1",
+ "AssessmentStatus": "Manual",
+ "Description": "Perform a periodic review of non-privileged role assignments to ensure that the non-privileged roles assigned to users are appropriate.",
+ "RationaleStatement": "To ensure the principle of least privilege is followed, non-privileged role assignments should be reviewed periodically to confirm that users are granted only the minimum level of permissions they need to perform their tasks.",
+ "ImpactStatement": "Increased administrative effort to manage and remove role assignments appropriately.",
+ "RemediationProcedure": "**Remediate from Azure Portal** 1. Go to `Subscriptions`. 2. Click the name of a subscription. 3. Click `Access control (IAM)`. 4. Click `Role assignments`. 5. Click `Job function roles`. 6. Check the box next to any inappropriate assignments. 7. Click `Delete`. 8. Click `Yes`. 9. Repeat steps 1-8 for each subscription.",
+ "AuditProcedure": "**Audit from Azure Portal** 1. Go to `Subscriptions`. 2. Click the name of a subscription. 3. Click `Access control (IAM)`. 4. Click `Role assignments`. 5. Click `Job function roles`. 6. For each role, ensure the assignments are appropriate. 7. Repeat steps 1-6 for each subscription.",
+ "AdditionalInformation": "",
+ "References": "https://learn.microsoft.com/en-us/azure/role-based-access-control/role-assignments",
+ "DefaultValue": "Users do not have non-privileged roles assigned to them by default."
+ }
+ ]
+ },
+ {
+ "Id": "5.4",
+ "Description": "Ensure that 'Restrict non-admin users from creating tenants' is set to 'Yes'",
+ "Checks": [
+ "entra_policy_ensure_default_user_cannot_create_tenants"
+ ],
+ "Attributes": [
+ {
+ "Section": "5 Identity Services",
+ "Profile": "Level 1",
+ "AssessmentStatus": "Automated",
+ "Description": "Require administrators or appropriately delegated users to create new tenants.",
+ "RationaleStatement": "It is recommended to only allow an administrator to create new tenants. This prevent users from creating new Microsoft Entra ID or Azure AD B2C tenants and ensures that only authorized users are able to do so.",
+ "ImpactStatement": "Enforcing this setting will ensure that only authorized users are able to create new tenants.",
+ "RemediationProcedure": "**Remediate from Azure Portal** 1. From Azure Home select the Portal Menu 1. Select `Microsoft Entra ID` 1. Under `Manage`, select `Users` 1. Under `Manage`, select `User settings` 1. Set `Restrict non-admin users from creating tenants ` to `Yes` 1. Click `Save` **Remediate from PowerShell** ``` Import-Module Microsoft.Graph.Identity.SignIns Connect-MgGraph -Scopes 'Policy.ReadWrite.Authorization' Select-MgProfile -Name beta $params = @{ DefaultUserRolePermissions = @{ AllowedToCreateTenants = $false } } Update-MgPolicyAuthorizationPolicy -AuthorizationPolicyId -BodyParameter $params ```",
+ "AuditProcedure": "**Audit from Azure Portal** 1. From Azure Home select the Portal Menu 1. Select `Microsoft Entra ID` 1. Under `Manage`, select `Users` 1. Under `Manage`, select `User settings` 1. Ensure that `Restrict non-admin users from creating tenants` is set to `Yes` **Audit from PowerShell** ``` Import-Module Microsoft.Graph.Identity.SignIns Connect-MgGraph -Scopes 'Policy.ReadWrite.Authorization' Get-MgPolicyAuthorizationPolicy | Select-Object -ExpandProperty DefaultUserRolePermissions | Format-List ``` Review the DefaultUserRolePermissions section of the output. Ensure that `AllowedToCreateTenants` is not `True`.",
+ "AdditionalInformation": "",
+ "References": "https://learn.microsoft.com/en-us/azure/active-directory/fundamentals/users-default-permissions:https://learn.microsoft.com/en-us/azure/active-directory/roles/permissions-reference#tenant-creator:https://blog.admindroid.com/disable-users-creating-new-azure-ad-tenants-in-microsoft-365/",
+ "DefaultValue": ""
+ }
+ ]
+ },
+ {
+ "Id": "5.5",
+ "Description": "Ensure that 'Number of methods required to reset' is set to '2'",
+ "Checks": [],
+ "Attributes": [
+ {
+ "Section": "5 Identity Services",
+ "Profile": "Level 1",
+ "AssessmentStatus": "Manual",
+ "Description": "Ensures that two alternate forms of identification are provided before allowing a password reset.",
+ "RationaleStatement": "A Self-service Password Reset (SSPR) through Azure Multi-factor Authentication (MFA) ensures the user's identity is confirmed using two separate methods of identification. With multiple methods set, an attacker would have to compromise both methods before they could maliciously reset a user's password.",
+ "ImpactStatement": "There may be administrative overhead, as users who lose access to their secondary authentication methods will need an administrator with permissions to remove it. There will also need to be organization-wide security policies and training to teach administrators to verify the identity of the requesting user so that social engineering cannot render this setting useless.",
+ "RemediationProcedure": "**Remediate from Azure Portal** 1. From Azure Home select the Portal Menu 1. Select `Microsoft Entra ID` 1. Under `Manage`, select `Users` 1. Under `Manage`, select `Password reset` 1. Select `Authentication methods` 1. Set the `Number of methods required to reset` to `2` 1. Click `Save`",
+ "AuditProcedure": "**Audit from Azure Portal** 1. From Azure Home select the Portal Menu 1. Select `Microsoft Entra ID` 1. Under `Manage`, select `Users` 1. Under `Manage`, select `Password reset` 1. Select `Authentication methods` 1. Ensure that `Number of methods required to reset` is set to `2`",
+ "AdditionalInformation": "",
+ "References": "https://learn.microsoft.com/en-us/entra/identity/authentication/tutorial-enable-sspr:https://learn.microsoft.com/en-us/entra/identity/authentication/concept-registration-mfa-sspr-combined:https://learn.microsoft.com/en-us/security/benchmark/azure/mcsb-identity-management#im-6-use-strong-authentication-controls:https://learn.microsoft.com/en-us/entra/identity/authentication/passwords-faq#password-reset-registration:https://support.microsoft.com/en-us/account-billing/reset-your-work-or-school-password-using-security-info-23dde81f-08bb-4776-ba72-e6b72b9dda9e:https://learn.microsoft.com/en-us/entra/identity/authentication/concept-authentication-methods",
+ "DefaultValue": "By default, the `Number of methods required to reset` is set to 2."
+ }
+ ]
+ },
+ {
+ "Id": "5.6",
+ "Description": "Ensure that account 'Lockout threshold' is less than or equal to '10'",
+ "Checks": [],
+ "Attributes": [
+ {
+ "Section": "5 Identity Services",
+ "Profile": "Level 1",
+ "AssessmentStatus": "Manual",
+ "Description": "The account lockout threshold determines how many failed login attempts are permitted prior to placing the account in a locked-out state and initiating a variable lockout duration.",
+ "RationaleStatement": "Account lockout is a method of protecting against brute-force and password spray attacks. Once the lockout threshold has been exceeded, the account enters a locked-out state which prevents all login attempts for a variable duration. The lockout in combination with a reasonable duration reduces the total number of failed login attempts that a malicious actor can execute in a given period of time.",
+ "ImpactStatement": "If account lockout threshold is set too low (less than 3), users may experience frequent lockout events and the resulting security alerts may contribute to alert fatigue. If account lockout threshold is set too high (more than 10), malicious actors can programmatically execute more password attempts in a given period of time.",
+ "RemediationProcedure": "**Remediate from Azure Portal** 1. From Azure Home select the Portal Menu. 1. Select `Microsoft Entra ID`. 1. Under `Manage`, select `Security`. 1. Under `Manage`, select `Authentication methods`. 1. Under `Manage`, select `Password protection`. 1. Set the `Lockout threshold` to `10` or fewer. 1. Click `Save`.",
+ "AuditProcedure": "**Audit from Azure Portal** 1. From Azure Home select the Portal Menu. 1. Select `Microsoft Entra ID`. 1. Under `Manage`, select `Security`. 1. Under `Manage`, select `Authentication methods`. 1. Under `Manage`, select `Password protection`. 1. Ensure that `Lockout threshold` is set to `10` or fewer.",
+ "AdditionalInformation": "**NOTE:** The variable number for failed login attempts allowed before lockout is prescribed by many security and compliance frameworks. The **appropriate** setting for this variable should be determined by the most restrictive security or compliance framework that your organization follows.",
+ "References": "https://learn.microsoft.com/en-us/entra/identity/authentication/howto-password-smart-lockout#manage-microsoft-entra-smart-lockout-values",
+ "DefaultValue": "By default, Lockout threshold is set to `10`."
+ }
+ ]
+ },
+ {
+ "Id": "5.7",
+ "Description": "Ensure that account 'Lockout duration in seconds' is greater than or equal to '60'",
+ "Checks": [],
+ "Attributes": [
+ {
+ "Section": "5 Identity Services",
+ "Profile": "Level 1",
+ "AssessmentStatus": "Manual",
+ "Description": "The account lockout duration value determines how long an account retains the status of lockout, and therefore how long before a user can continue to attempt to login after passing the lockout threshold.",
+ "RationaleStatement": "Account lockout is a method of protecting against brute-force and password spray attacks. Once the lockout threshold has been exceeded, the account enters a locked-out state which prevents all login attempts for a variable duration. The lockout in combination with a reasonable duration reduces the total number of failed login attempts that a malicious actor can execute in a given period of time.",
+ "ImpactStatement": "If account lockout duration is set too low (less than 60 seconds), malicious actors can perform more password spray and brute-force attempts over a given period of time. If the account lockout duration is set too high (more than 300 seconds) users may experience inconvenient delays during lockout.",
+ "RemediationProcedure": "**Remediate from Azure Portal** 1. From Azure Home select the Portal Menu. 1. Select `Microsoft Entra ID`. 1. Under `Manage`, select `Security`. 1. Under `Manage`, select `Authentication methods`. 1. Under `Manage`, select `Password protection`. 1. Set the `Lockout duration in seconds` to `60` or higher. 1. Click `Save`.",
+ "AuditProcedure": "**Audit from Azure Portal** 1. From Azure Home select the Portal Menu. 1. Select `Microsoft Entra ID`. 1. Under `Manage`, select `Security`. 1. Under `Manage`, select `Authentication methods`. 1. Under `Manage`, select `Password protection`. 1. Ensure that `Lockout duration in seconds` is set to `60` or higher.",
+ "AdditionalInformation": "",
+ "References": "https://learn.microsoft.com/en-us/entra/identity/authentication/howto-password-smart-lockout#manage-microsoft-entra-smart-lockout-values",
+ "DefaultValue": "By default, Lockout duration in seconds is set to `60`."
+ }
+ ]
+ },
+ {
+ "Id": "5.8",
+ "Description": "Ensure that a 'Custom banned password list' is set to 'Enforce'",
+ "Checks": [],
+ "Attributes": [
+ {
+ "Section": "5 Identity Services",
+ "Profile": "Level 1",
+ "AssessmentStatus": "Manual",
+ "Description": "Microsoft Azure applies a default global banned password list to all user and admin accounts that are created and managed directly in Microsoft Entra ID. The Microsoft Entra password policy does not apply to user accounts that are synchronized from an on-premises Active Directory environment, unless Microsoft Entra ID Connect is used and `EnforceCloudPasswordPolicyForPasswordSyncedUsers` is enabled. Review the `Default Value` section for more detail on the password policy. For increased password security, a custom banned password list is recommended",
+ "RationaleStatement": "Implementing a custom banned password list gives your organization further control over the password policy. Disallowing easy-to-guess passwords increases the security of your Azure resources.",
+ "ImpactStatement": "Increasing password complexity may increase user account administration overhead. Utilizing the default global banned password list and a custom list requires a Microsoft Entra ID P1 or P2 license. On-premises Active Directory Domain Services users who aren't synchronized to Microsoft Entra ID still benefit from Microsoft Entra ID Password Protection based on the existing licensing of synchronized users.",
+ "RemediationProcedure": "**Remediate from Azure Portal** 1. From Azure Home select the Portal Menu. 1. Select `Microsoft Entra ID`. 1. Under `Manage`, select `Security`. 1. Under `Manage`, select `Authentication methods`. 1. Under `Manage`, select `Password protection`. 1. Set the `Enforce custom list` option to `Yes`. 1. Click in the `Custom banned password list` text box. 1. Add a list of words, one per line, to prevent users from using in passwords. 1. Click `Save`.",
+ "AuditProcedure": "**Audit from Azure Portal** 1. From Azure Home select the Portal Menu. 1. Select `Microsoft Entra ID`. 1. Under `Manage`, select `Security`. 1. Under `Manage`, select `Authentication methods`. 1. Under `Manage`, select `Password protection`. 1. Ensure `Enforce custom list` is set to `Yes`. 1. Review the list of words banned from use in passwords.",
+ "AdditionalInformation": "",
+ "References": "https://learn.microsoft.com/en-us/entra/identity/authentication/concept-password-ban-bad-combined-policy:https://learn.microsoft.com/en-us/entra/identity/authentication/concept-password-ban-bad:https://docs.microsoft.com/en-us/powershell/module/Azuread/:https://www.microsoft.com/en-us/research/publication/password-guidance/:https://learn.microsoft.com/en-us/entra/identity/authentication/tutorial-configure-custom-password-protection:https://learn.microsoft.com/en-us/security/benchmark/azure/mcsb-identity-management#im-6-use-strong-authentication-controls",
+ "DefaultValue": "By default the custom banned password list is not 'Enabled'. Organization-specific terms can be added to the custom banned password list, such as the following examples: - Brand names - Product names - Locations, such as company headquarters - Company-specific terms - Abbreviations that have specific company meaning - Months and weekdays with your company's local languages The default global banned password list is already applied to your resources which applies the following basic requirements: **Characters allowed:** - Uppercase characters (A - Z) - Lowercase characters (a - z) - Numbers (0 - 9) - Symbols: - @ # $ % ^ & * - _ ! + = [ ] { } | \\ : ' , . ? / ` ~ ( ) ; < > - blank space **Characters not allowed:** - Unicode characters **Password length:** Passwords require: - A minimum of eight characters - A maximum of 256 characters **Password complexity:** Passwords require three out of four of the following categories: - Uppercase characters - Lowercase characters - Numbers - Symbols Note: Password complexity check isn't required for Education tenants. **Password not recently used:** - When a user changes or resets their password, the new password can't be the same as the current or recently used passwords. - Password isn't banned by Entra ID Password Protection. - The password can't be on the global list of banned passwords for Azure AD Password Protection, or on the customizable list of banned passwords specific to your organization. **Evaluation** New passwords are evaluated for strength and complexity by validating against the combined list of terms from the global and custom banned password lists. Even if a user's password contains a banned password, the password may be accepted if the overall password is otherwise strong enough."
+ }
+ ]
+ },
+ {
+ "Id": "5.9",
+ "Description": "Ensure that 'Number of days before users are asked to re-confirm their authentication information' is not set to '0'",
+ "Checks": [],
+ "Attributes": [
+ {
+ "Section": "5 Identity Services",
+ "Profile": "Level 1",
+ "AssessmentStatus": "Manual",
+ "Description": "Ensure that the number of days before users are asked to re-confirm their authentication information is not set to 0.",
+ "RationaleStatement": "This setting is necessary if 'Require users to register when signing in' is enabled. If authentication re-confirmation is disabled, registered users will never be prompted to re-confirm their existing authentication information. If the authentication information for a user changes, such as a phone number or email, then the password reset information for that user reverts to the previously registered authentication information.",
+ "ImpactStatement": "Users will be prompted to re-confirm their authentication information after the number of days specified.",
+ "RemediationProcedure": "**Remediate from Azure Portal** 1. From Azure Home select the Portal Menu. 1. Select `Microsoft Entra ID`. 1. Under `Manage`, select `Users`. 1. Select `Password reset`. 1. Under `Manage`, select `Registration`. 1. Set the `Number of days before users are asked to re-confirm their authentication information` to your organization-defined frequency. 1. Click `Save`.",
+ "AuditProcedure": "**Audit from Azure Portal** 1. From Azure Home select the Portal Menu. 1. Select `Microsoft Entra ID`. 1. Under `Manage`, select `Users`. 1. Select `Password reset`. 1. Under `Manage`, select `Registration`. 1. Ensure that `Number of days before users are asked to re-confirm their authentication information` is not set to `0`.",
+ "AdditionalInformation": "",
+ "References": "https://learn.microsoft.com/en-us/entra/identity/authentication/concept-sspr-howitworks#registration:https://support.microsoft.com/en-us/account-billing/reset-your-work-or-school-password-using-security-info-23dde81f-08bb-4776-ba72-e6b72b9dda9e:https://learn.microsoft.com/en-us/security/benchmark/azure/mcsb-governance-strategy#gs-6-define-and-implement-identity-and-privileged-access-strategy:https://learn.microsoft.com/en-us/entra/identity/authentication/concept-authentication-methods",
+ "DefaultValue": "By default, the `Number of days before users are asked to re-confirm their authentication information` is set to 180 days."
+ }
+ ]
+ },
+ {
+ "Id": "5.10",
+ "Description": "Ensure that 'Notify users on password resets?' is set to 'Yes'",
+ "Checks": [],
+ "Attributes": [
+ {
+ "Section": "5 Identity Services",
+ "Profile": "Level 1",
+ "AssessmentStatus": "Manual",
+ "Description": "Ensure that users are notified on their primary and alternate emails on password resets.",
+ "RationaleStatement": "User notification on password reset is a proactive way of confirming password reset activity. It helps the user to recognize unauthorized password reset activities.",
+ "ImpactStatement": "Users will receive emails alerting them to password changes to both their primary and alternate emails.",
+ "RemediationProcedure": "**Remediate from Azure Portal** 1. From Azure Home select the Portal Menu 1. Select `Microsoft Entra ID` 1. Under `Manage`, select `Users` 1. Under `Manage`, select `Password reset` 1. Under `Manage`, select `Notifications` 1. Set `Notify users on password resets?` to `Yes` 1. Click `Save`",
+ "AuditProcedure": "**Audit from Azure Portal** 1. From Azure Home select the Portal Menu 1. Select `Microsoft Entra ID` 1. Under `Manage`, select `Users` 1. Under `Manage`, select `Password reset` 1. Under `Manage`, select `Notifications` 1. Ensure that `Notify users on password resets?` is set to `Yes`",
+ "AdditionalInformation": "",
+ "References": "https://learn.microsoft.com/en-us/entra/identity/authentication/tutorial-enable-sspr#set-up-notifications-and-customizations:https://learn.microsoft.com/en-us/entra/identity/authentication/concept-sspr-howitworks#notifications:https://support.microsoft.com/en-us/account-billing/reset-your-work-or-school-password-using-security-info-23dde81f-08bb-4776-ba72-e6b72b9dda9e:https://learn.microsoft.com/en-us/security/benchmark/azure/mcsb-governance-strategy#gs-6-define-and-implement-identity-and-privileged-access-strategy",
+ "DefaultValue": "By default, `Notify users on password resets?` is set to Yes."
+ }
+ ]
+ },
+ {
+ "Id": "5.11",
+ "Description": "Ensure that 'Notify all admins when other admins reset their password?' is set to 'Yes'",
+ "Checks": [],
+ "Attributes": [
+ {
+ "Section": "5 Identity Services",
+ "Profile": "Level 1",
+ "AssessmentStatus": "Manual",
+ "Description": "Ensure that all Global Administrators are notified if any other administrator resets their password.",
+ "RationaleStatement": "Administrator accounts are sensitive. Any password reset activity notification, when sent to all Administrators, ensures that all Global Administrators can passively confirm if such a reset is a common pattern within their group. For example, if all Administrators change their password every 30 days, any password reset activity before that may require administrator(s) to evaluate any unusual activity and confirm its origin.",
+ "ImpactStatement": "All Global Administrators will receive a notification from Azure every time a password is reset. This is useful for auditing procedures to confirm that there are no out of the ordinary password resets for Administrators. There is additional overhead, however, in the time required for Global Administrators to audit the notifications. This setting is only useful if all Global Administrators pay attention to the notifications and audit each one.",
+ "RemediationProcedure": "**Remediate from Azure Portal** 1. From Azure Home select the Portal Menu 1. Select `Microsoft Entra ID` 1. Under `Manage`, select `Users` 1. Under `Manage`, select `Password reset` 1. Under `Manage`, select `Notifications` 1. Set `Notify all admins when other admins reset their password?` to `Yes` 1. Click `Save`",
+ "AuditProcedure": "**Audit from Azure Portal** 1. From Azure Home select the Portal Menu 1. Select `Microsoft Entra ID` 1. Under `Manage`, select `Users` 1. Under `Manage`, select `Password reset` 1. Under `Manage`, select `Notifications` 1. Ensure that `Notify all admins when other admins reset their password?` is set to `Yes`",
+ "AdditionalInformation": "",
+ "References": "https://learn.microsoft.com/en-us/entra/identity/authentication/concept-sspr-howitworks#notifications:https://support.microsoft.com/en-us/account-billing/reset-your-work-or-school-password-using-security-info-23dde81f-08bb-4776-ba72-e6b72b9dda9e:https://learn.microsoft.com/en-us/security/benchmark/azure/mcsb-governance-strategy#gs-6-define-and-implement-identity-and-privileged-access-strategy:https://learn.microsoft.com/en-us/security/benchmark/azure/mcsb-privileged-access#pa-1-separate-and-limit-highly-privilegedadministrative-users:https://learn.microsoft.com/en-us/entra/identity/authentication/tutorial-enable-sspr#set-up-notifications-and-customizations",
+ "DefaultValue": "By default, `Notify all admins when other admins reset their password?` is set to No."
+ }
+ ]
+ },
+ {
+ "Id": "5.12",
+ "Description": "Ensure that 'User consent for applications' is set to 'Do not allow user consent'",
+ "Checks": [
+ "entra_policy_restricts_user_consent_for_apps"
+ ],
+ "Attributes": [
+ {
+ "Section": "5 Identity Services",
+ "Profile": "Level 1",
+ "AssessmentStatus": "Manual",
+ "Description": "Require administrators to provide consent for applications before use.",
+ "RationaleStatement": "If Microsoft Entra ID is running as an identity provider for third-party applications, permissions and consent should be limited to administrators or pre-approved. Malicious applications may attempt to exfiltrate data or abuse privileged user accounts.",
+ "ImpactStatement": "Enforcing this setting may create additional requests that administrators need to review.",
+ "RemediationProcedure": "**Remediate from Azure Portal** 1. From Azure Home select the Portal Menu 1. Select `Microsoft Entra ID` 1. Under `Manage`, select `Enterprise applications` 1. Under `Security`, select `Consent and permissions` 1. Under `Manage`, select `User consent settings` 1. Set `User consent for applications` to `Do not allow user consent` 1. Click `Save`",
+ "AuditProcedure": "**Audit from Azure Portal** 1. From Azure Home select the Portal Menu 1. Select `Microsoft Entra ID` 1. Under `Manage`, select `Enterprise applications` 1. Under `Security`, select `Consent and permissions` 1. Under `Manage`, select `User consent settings` 1. Ensure `User consent for applications` is set to `Do not allow user consent` **Audit from PowerShell** ``` Connect-MgGraph (Get-MgPolicyAuthorizationPolicy).DefaultUserRolePermissions | Select-Object -ExpandProperty PermissionGrantPoliciesAssigned ``` If the command returns no values in response, the configuration complies with the recommendation.",
+ "AdditionalInformation": "",
+ "References": "https://learn.microsoft.com/en-us/entra/identity/enterprise-apps/configure-user-consent?pivots=ms-powershell#configure-user-consent-to-applications:https://learn.microsoft.com/en-us/security/benchmark/azure/mcsb-privileged-access#pa-1-separate-and-limit-highly-privilegedadministrative-users:https://learn.microsoft.com/en-us/security/benchmark/azure/mcsb-governance-strategy#gs-2-define-and-implement-enterprise-segmentationseparation-of-duties-strategy:https://learn.microsoft.com/en-us/security/benchmark/azure/mcsb-governance-strategy#gs-6-define-and-implement-identity-and-privileged-access-strategy",
+ "DefaultValue": "By default, `Users consent for applications` is set to `Allow user consent for apps`."
+ }
+ ]
+ },
+ {
+ "Id": "5.13",
+ "Description": "Ensure that 'User consent for applications' is set to 'Allow user consent for apps from verified publishers, for selected permissions'",
+ "Checks": [
+ "entra_policy_user_consent_for_verified_apps"
+ ],
+ "Attributes": [
+ {
+ "Section": "5 Identity Services",
+ "Profile": "Level 2",
+ "AssessmentStatus": "Manual",
+ "Description": "Allow users to provide consent for selected permissions when a request is coming from a verified publisher.",
+ "RationaleStatement": "If Microsoft Entra ID is running as an identity provider for third-party applications, permissions and consent should be limited to administrators or pre-approved. Malicious applications may attempt to exfiltrate data or abuse privileged user accounts.",
+ "ImpactStatement": "Enforcing this setting may create additional requests that administrators need to review.",
+ "RemediationProcedure": "**Remediate from Azure Portal** 1. From Azure Home select the Portal Menu 1. Select `Microsoft Entra ID` 1. Under `Manage`, select `Enterprise applications` 1. Under `Security, select `Consent and permissions` 1. Under `Manage`, select `User consent settings` 1. Under `User consent for applications`, select `Allow user consent for apps from verified publishers, for selected permissions` 1. Click `Save`",
+ "AuditProcedure": "**Audit from Azure Portal** 1. From Azure Home select the Portal Menu 1. Select `Microsoft Entra ID` 1. Under `Manage`, select `Enterprise applications` 1. Under `Security, select `Consent and permissions` 1. Under `Manage`, select `User consent settings` 1. Under `User consent for applications`, ensure `Allow user consent for apps from verified publishers, for selected permissions` is selected **Audit from PowerShell** ``` Connect-MgGraph (Get-MgPolicyAuthorizationPolicy).DefaultUserRolePermissions | Select-Object -ExpandProperty PermissionGrantPoliciesAssigned ``` The command should return either `ManagePermissionGrantsForSelf.microsoft-user-default-low` or a custom app consent policy id if one is in use.",
+ "AdditionalInformation": "",
+ "References": "https://learn.microsoft.com/en-us/entra/identity/enterprise-apps/configure-user-consent?pivots=ms-graph#configure-user-consent-to-applications:https://learn.microsoft.com/en-us/security/benchmark/azure/mcsb-privileged-access#pa-1-separate-and-limit-highly-privilegedadministrative-users:https://learn.microsoft.com/en-us/security/benchmark/azure/mcsb-governance-strategy#gs-2-define-and-implement-enterprise-segmentationseparation-of-duties-strategy:https://learn.microsoft.com/en-us/security/benchmark/azure/mcsb-governance-strategy#gs-6-define-and-implement-identity-and-privileged-access-strategy:https://learn.microsoft.com/en-us/powershell/module/microsoft.graph.identity.signins/get-mgpolicyauthorizationpolicy?view=graph-powershell-1.0",
+ "DefaultValue": "By default, `User consent for applications` is set to `Allow user consent for apps`."
+ }
+ ]
+ },
+ {
+ "Id": "5.14",
+ "Description": "Ensure that 'Users can register applications' is set to 'No'",
+ "Checks": [
+ "entra_policy_ensure_default_user_cannot_create_apps"
+ ],
+ "Attributes": [
+ {
+ "Section": "5 Identity Services",
+ "Profile": "Level 1",
+ "AssessmentStatus": "Automated",
+ "Description": "Require administrators or appropriately delegated users to register third-party applications.",
+ "RationaleStatement": "It is recommended to only allow an administrator to register custom-developed applications. This ensures that the application undergoes a formal security review and approval process prior to exposing Microsoft Entra ID data. Certain users like developers or other high-request users may also be delegated permissions to prevent them from waiting on an administrative user. Your organization should review your policies and decide your needs.",
+ "ImpactStatement": "Enforcing this setting will create additional requests for approval that will need to be addressed by an administrator. If permissions are delegated, a user may approve a malevolent third party application, potentially giving it access to your data.",
+ "RemediationProcedure": "**Remediate from Azure Portal** 1. From Azure Home select the Portal Menu 1. Select `Microsoft Entra ID` 1. Under `Manage`, select `Users` 1. Under `Manage`, select `User settings` 1. Set `Users can register applications` to `No` 1. Click `Save` **Remediate from PowerShell** ``` $param = @{ AllowedToCreateApps = $false } Update-MgPolicyAuthorizationPolicy -DefaultUserRolePermissions $param ```",
+ "AuditProcedure": "**Audit from Azure Portal** 1. From Azure Home select the Portal Menu 1. Select `Microsoft Entra ID` 1. Under `Manage`, select `Users` 1. Under `Manage`, select `User settings` 1. Ensure that `Users can register applications` is set to `No` **Audit from PowerShell** ``` (Get-MgPolicyAuthorizationPolicy).DefaultUserRolePermissions | Format-List AllowedToCreateApps ``` Command should return the value of `False`",
+ "AdditionalInformation": "",
+ "References": "https://learn.microsoft.com/en-us/entra/identity/role-based-access-control/delegate-app-roles#restrict-who-can-create-applications:https://learn.microsoft.com/en-us/entra/identity-platform/how-applications-are-added#who-has-permission-to-add-applications-to-my-azure-ad-instance:https://learn.microsoft.com/en-us/security/benchmark/azure/mcsb-governance-strategy#gs-6-define-and-implement-identity-and-privileged-access-strategy:https://learn.microsoft.com/en-us/security/benchmark/azure/mcsb-privileged-access#pa-1-separate-and-limit-highly-privilegedadministrative-users:https://learn.microsoft.com/en-us/powershell/module/microsoft.graph.identity.signins/get-mgpolicyauthorizationpolicy?view=graph-powershell-1.0",
+ "DefaultValue": "By default, `Users can register applications` is set to Yes."
+ }
+ ]
+ },
+ {
+ "Id": "5.15",
+ "Description": "Ensure that 'Guest users access restrictions' is set to 'Guest user access is restricted to properties and memberships of their own directory objects'",
+ "Checks": [
+ "entra_policy_guest_users_access_restrictions"
+ ],
+ "Attributes": [
+ {
+ "Section": "5 Identity Services",
+ "Profile": "Level 1",
+ "AssessmentStatus": "Automated",
+ "Description": "Limit guest user permissions.",
+ "RationaleStatement": "Limiting guest access ensures that guest accounts do not have permission for certain directory tasks, such as enumerating users, groups or other directory resources, and cannot be assigned to administrative roles in your directory. Guest access has three levels of restriction. 1. Guest users have the same access as members (most inclusive), 2. Guest users have limited access to properties and memberships of directory objects (default value), 3. Guest user access is restricted to properties and memberships of their own directory objects (most restrictive). The recommended option is the 3rd, most restrictive: Guest user access is restricted to their own directory object.",
+ "ImpactStatement": "This may create additional requests for permissions to access resources that administrators will need to approve. According to https://learn.microsoft.com/en-us/azure/active-directory/enterprise-users/users-restrict-guest-permissions#services-currently-not-supported Service without current support might have compatibility issues with the new guest restriction setting. - Forms - Project - Yammer - Planner in SharePoint",
+ "RemediationProcedure": "**Remediate from Azure Portal** 1. From Azure Home select the Portal Menu 1. Select `Microsoft Entra ID` 1. Under `Manage`, select `External Identities` 1. Select `External collaboration settings` 1. Under `Guest user access`, set `Guest user access restrictions` to `Guest user access is restricted to properties and memberships of their own directory objects` 1. Click `Save` **Remediate from PowerShell** 1. Enter the following to update the policy ID: ``` Update-MgPolicyAuthorizationPolicy -GuestUserRoleId 2af84b1e-32c8-42b7-82bc-daa82404023b ``` 1. Check the GuestUserRoleId again: ``` (Get-MgPolicyAuthorizationPolicy).GuestUserRoleId ``` 1. Ensure that the GuestUserRoleId is equal to the earlier entered value of `2af84b1e-32c8-42b7-82bc-daa82404023b`.",
+ "AuditProcedure": "**Audit from Azure Portal** 1. From Azure Home select the Portal Menu 1. Select `Microsoft Entra ID` 1. Under `Manage`, select `External Identities` 1. Select `External collaboration settings` 1. Under `Guest user access`, ensure that `Guest user access restrictions ` is set to `Guest user access is restricted to properties and memberships of their own directory objects` **Audit from PowerShell** 1. Enter the following: ``` Connect-MgGraph (Get-MgPolicyAuthorizationPolicy).GuestUserRoleId ``` Which will give a result like: ``` Id : authorizationPolicy OdataType : Description : Used to manage authorization related settings across the company. DisplayName : Authorization Policy EnabledPreviewFeatures : {} GuestUserRoleId : 10dae51f-b6af-4016-8d66-8c2a99b929b3 PermissionGrantPolicyIdsAssignedToDefaultUserRole : {user-default-legacy} ``` If the GuestUserRoleID property does not equal `2af84b1e-32c8-42b7-82bc-daa82404023b` then it is not set to most restrictive.",
+ "AdditionalInformation": "",
+ "References": "https://learn.microsoft.com/en-us/entra/fundamentals/users-default-permissions#member-and-guest-users:https://learn.microsoft.com/en-us/security/benchmark/azure/mcsb-privileged-access#pa-3-manage-lifecycle-of-identities-and-entitlements:https://learn.microsoft.com/en-us/security/benchmark/azure/mcsb-governance-strategy#gs-2-define-and-implement-enterprise-segmentationseparation-of-duties-strategy:https://learn.microsoft.com/en-us/security/benchmark/azure/mcsb-governance-strategy#gs-6-define-and-implement-identity-and-privileged-access-strategy:https://learn.microsoft.com/en-us/entra/identity/users/users-restrict-guest-permissions",
+ "DefaultValue": "By default, `Guest user access restrictions` is set to `Guest users have limited access to properties and memberships of directory objects`."
+ }
+ ]
+ },
+ {
+ "Id": "5.16",
+ "Description": "Ensure that 'Guest invite restrictions' is set to 'Only users assigned to specific admin roles can invite guest users' or 'No one in the organization can invite guest users'",
+ "Checks": [
+ "entra_policy_guest_invite_only_for_admin_roles"
+ ],
+ "Attributes": [
+ {
+ "Section": "5 Identity Services",
+ "Profile": "Level 2",
+ "AssessmentStatus": "Automated",
+ "Description": "Restrict invitations to either users with specific administrative roles or no one.",
+ "RationaleStatement": "Restricting invitations to users with specific administrator roles ensures that only authorized accounts have access to cloud resources. This helps to maintain 'Need to Know' permissions and prevents inadvertent access to data. By default the setting is set to 'Anyone in the organization can invite guest users including guests and non-admins', which poses a security risk.",
+ "ImpactStatement": "With the option of 'Only users assigned to specific admin roles can invite guest users' selected, users with specific admin roles will be in charge of sending invitations to external users, requiring additional overhead to manage user accounts.",
+ "RemediationProcedure": "**Remediate from Azure Portal** 1. From Azure Home select the Portal Menu. 2. Select `Microsoft Entra ID`. 3. Under `Manage`, select `External Identities`. 4. Select `External collaboration settings`. 5. Under `Guest invite settings`, set `Guest invite restrictions` to either `Only users assigned to specific admin roles can invite guest users` or `No one in the organization [...]`. 6. Click `Save`. **Remediate from PowerShell** ``` Connect-MgGraph Update-MgPolicyAuthorizationPolicy -AllowInvitesFrom \"adminsAndGuestInviters\" ``` Or for most restrictive: ``` Update-MgPolicyAuthorizationPolicy -AllowInvitesFrom \"none\" ```",
+ "AuditProcedure": "**Audit from Azure Portal** 1. From Azure Home select the Portal Menu. 2. Select `Microsoft Entra ID`. 3. Under `Manage`, select `External Identities`. 4. Select `External collaboration settings`. 5. Under `Guest invite settings`, ensure that `Guest invite restrictions` is set to either `Only users assigned to specific admin roles can invite guest users` or `No one in the organization [...]`. **Audit from PowerShell** ``` Connect-MgGraph (Get-MgPolicyAuthorizationPolicy).AllowInvitesFrom ``` If the resulting value is `adminsAndGuestInviters` or `none` the configuration complies.",
+ "AdditionalInformation": "",
+ "References": "https://learn.microsoft.com/en-us/entra/external-id/external-collaboration-settings-configure:https://learn.microsoft.com/en-us/security/benchmark/azure/mcsb-governance-strategy#gs-6-define-and-implement-identity-and-privileged-access-strategy",
+ "DefaultValue": "By default, Guest invite restrictions is set to 'Anyone in the organization can invite guest users including guests and non-admins'."
+ }
+ ]
+ },
+ {
+ "Id": "5.17",
+ "Description": "Ensure that 'Restrict access to Microsoft Entra admin center' is set to 'Yes'",
+ "Checks": [],
+ "Attributes": [
+ {
+ "Section": "5 Identity Services",
+ "Profile": "Level 1",
+ "AssessmentStatus": "Manual",
+ "Description": "Restrict access to the Microsoft Entra ID administration center to administrators only. **NOTE**: This only affects access to the Entra ID administrator's web portal. This setting does not prohibit privileged users from using other methods such as Rest API or Powershell to obtain sensitive information from Microsoft Entra ID.",
+ "RationaleStatement": "The Microsoft Entra ID administrative center has sensitive data and permission settings. All non-administrators should be prohibited from accessing any Microsoft Entra ID data in the administration center to avoid exposure.",
+ "ImpactStatement": "All administrative tasks will need to be done by Administrators, causing additional overhead in management of users and resources.",
+ "RemediationProcedure": "**Remediate from Azure Portal** 1. From Azure Home select the Portal Menu 1. Select `Microsoft Entra ID` 1. Under `Manage`, select `Users` 1. Under `Manage`, select `User settings` 1. Under `Administration centre`, set `Restrict access to Microsoft Entra admin center` to `Yes` 1. Click `Save`",
+ "AuditProcedure": "**Audit from Azure Portal** 1. From Azure Home select the Portal Menu 1. Select `Microsoft Entra ID` 1. Under `Manage`, select `Users` 1. Under `Manage`, select `User settings` 1. Under `Administration centre`, ensure that `Restrict access to Microsoft Entra admin center` is set to `Yes`",
+ "AdditionalInformation": "",
+ "References": "https://docs.microsoft.com/en-us/azure/active-directory/active-directory-assign-admin-roles-azure-portal:https://learn.microsoft.com/en-us/security/benchmark/azure/mcsb-governance-strategy#gs-2-define-and-implement-enterprise-segmentationseparation-of-duties-strategy:https://learn.microsoft.com/en-us/security/benchmark/azure/mcsb-governance-strategy#gs-6-define-and-implement-identity-and-privileged-access-strategy:https://learn.microsoft.com/en-us/security/benchmark/azure/mcsb-privileged-access#pa-1-separate-and-limit-highly-privilegedadministrative-users",
+ "DefaultValue": "By default, `Restrict access to Microsoft Entra admin center` is set to `No`"
+ }
+ ]
+ },
+ {
+ "Id": "5.18",
+ "Description": "Ensure that 'Restrict user ability to access groups features in My Groups' is set to 'Yes'",
+ "Checks": [],
+ "Attributes": [
+ {
+ "Section": "5 Identity Services",
+ "Profile": "Level 1",
+ "AssessmentStatus": "Manual",
+ "Description": "Restrict access to group web interface in the Access Panel portal.",
+ "RationaleStatement": "Self-service group management enables users to create and manage security groups or Office 365 groups in Microsoft Entra ID. Unless a business requires this day-to-day delegation for some users, self-service group management should be disabled. Any user can access the Access Panel, where they can reset their passwords, view their information, etc. By default, users are also allowed to access the Group feature, which shows groups, members, related resources (SharePoint URL, Group email address, Yammer URL, and Teams URL). By setting this feature to 'Yes', users will no longer have access to the web interface, but still have access to the data using the API. This is useful to prevent non-technical users from enumerating groups-related information, but technical users will still be able to access this information using APIs.",
+ "ImpactStatement": "Setting to `Yes` could create administrative overhead by customers seeking certain group memberships that will have to be manually managed by administrators with appropriate permissions.",
+ "RemediationProcedure": "**Remediate from Azure Portal** 1. From Azure Home select the Portal Menu 1. Select `Microsoft Entra ID` 1. Under `Manage`, select `Groups` 1. Under `Settings`, select `General` 1. Under `Self Service Group Management`, set `Restrict user ability to access groups features in My Groups` to `Yes` 1. Click `Save`",
+ "AuditProcedure": "**Audit from Azure Portal** 1. From Azure Home select the Portal Menu 1. Select `Microsoft Entra ID` 1. Under `Manage`, select `Groups` 1. Under `Settings`, select `General` 1. Under `Self Service Group Management`, ensure that `Restrict user ability to access groups features in My Groups` is set to `Yes`",
+ "AdditionalInformation": "",
+ "References": "https://learn.microsoft.com/en-us/entra/identity/users/groups-self-service-management:https://learn.microsoft.com/en-us/security/benchmark/azure/mcsb-privileged-access#pa-1-separate-and-limit-highly-privilegedadministrative-users:https://learn.microsoft.com/en-us/security/benchmark/azure/mcsb-privileged-access#pa-3-manage-lifecycle-of-identities-and-entitlements:https://learn.microsoft.com/en-us/security/benchmark/azure/mcsb-governance-strategy#gs-2-define-and-implement-enterprise-segmentationseparation-of-duties-strategy:https://learn.microsoft.com/en-us/security/benchmark/azure/mcsb-governance-strategy#gs-6-define-and-implement-identity-and-privileged-access-strategy",
+ "DefaultValue": "By default, `Restrict user ability to access groups features in the Access Pane` is set to `No`"
+ }
+ ]
+ },
+ {
+ "Id": "5.19",
+ "Description": "Ensure that 'Users can create security groups in Azure portals, API or PowerShell' is set to 'No'",
+ "Checks": [
+ "entra_policy_default_users_cannot_create_security_groups"
+ ],
+ "Attributes": [
+ {
+ "Section": "5 Identity Services",
+ "Profile": "Level 1",
+ "AssessmentStatus": "Manual",
+ "Description": "Restrict security group creation to administrators only.",
+ "RationaleStatement": "When creating security groups is enabled, all users in the directory are allowed to create new security groups and add members to those groups. Unless a business requires this day-to-day delegation, security group creation should be restricted to administrators only.",
+ "ImpactStatement": "Enabling this setting could create a number of requests that would need to be managed by an administrator.",
+ "RemediationProcedure": "**Remediate from Azure Portal** 1. From Azure Home select the Portal Menu 1. Select `Microsoft Entra ID` 1. Under `Manage`, select `Groups` 1. Under `Settings`, select `General` 1. Under `Security Groups`, set `Users can create security groups in Azure portals, API or PowerShell` to `No` 1. Click `Save`",
+ "AuditProcedure": "**Audit from Azure Portal** 1. From Azure Home select the Portal Menu 1. Select `Microsoft Entra ID` 1. Under `Manage`, select `Groups` 1. Under `Settings`, select `General` 1. Under `Security Groups`, ensure that `Users can create security groups in Azure portals, API or PowerShell` is set to `No`",
+ "AdditionalInformation": "",
+ "References": "https://learn.microsoft.com/en-us/entra/identity/users/groups-self-service-management#making-a-group-available-for-end-user-self-service:https://learn.microsoft.com/en-us/security/benchmark/azure/mcsb-governance-strategy#gs-6-define-and-implement-identity-and-privileged-access-strategy:https://learn.microsoft.com/en-us/security/benchmark/azure/mcsb-governance-strategy#gs-2-define-and-implement-enterprise-segmentationseparation-of-duties-strategy:https://learn.microsoft.com/en-us/security/benchmark/azure/mcsb-privileged-access#pa-1-separate-and-limit-highly-privilegedadministrative-users:https://learn.microsoft.com/en-us/security/benchmark/azure/mcsb-privileged-access#pa-3-manage-lifecycle-of-identities-and-entitlements",
+ "DefaultValue": "By default, `Users can create security groups in Azure portals, API or PowerShell` is set to `Yes`"
+ }
+ ]
+ },
+ {
+ "Id": "5.20",
+ "Description": "Ensure that 'Owners can manage group membership requests in My Groups' is set to 'No'",
+ "Checks": [],
+ "Attributes": [
+ {
+ "Section": "5 Identity Services",
+ "Profile": "Level 1",
+ "AssessmentStatus": "Manual",
+ "Description": "Restrict security group management to administrators only.",
+ "RationaleStatement": "Restricting security group management to administrators only prohibits users from making changes to security groups. This ensures that security groups are appropriately managed and their management is not delegated to non-administrators.",
+ "ImpactStatement": "Group Membership for user accounts will need to be handled by Admins and cause administrative overhead.",
+ "RemediationProcedure": "**Remediate from Azure Portal** 1. From Azure Home select the Portal Menu 1. Select `Microsoft Entra ID` 1. Under `Manage`, select `Groups` 1. Under `Settings`, select `General` 1. Under `Self Service Group Management`, set `Owners can manage group membership requests in My Groups` to `No` 1. Click `Save`",
+ "AuditProcedure": "**Audit from Azure Portal** 1. From Azure Home select the Portal Menu 1. Select `Microsoft Entra ID` 1. Under `Manage`, select `Groups` 1. Under `Settings`, select `General` 1. Under `Self Service Group Management`, ensure that `Owners can manage group membership requests in My Groups` is set to `No`",
+ "AdditionalInformation": "",
+ "References": "https://learn.microsoft.com/en-us/entra/identity/users/groups-self-service-management#making-a-group-available-for-end-user-self-service:https://learn.microsoft.com/en-us/security/benchmark/azure/mcsb-privileged-access#pa-1-separate-and-limit-highly-privilegedadministrative-users:https://learn.microsoft.com/en-us/security/benchmark/azure/mcsb-privileged-access#pa-3-manage-lifecycle-of-identities-and-entitlements:https://learn.microsoft.com/en-us/security/benchmark/azure/mcsb-privileged-access#pa-8-determine-access-process-for-cloud-provider-support:https://learn.microsoft.com/en-us/security/benchmark/azure/mcsb-governance-strategy#gs-2-define-and-implement-enterprise-segmentationseparation-of-duties-strategy:https://learn.microsoft.com/en-us/security/benchmark/azure/mcsb-governance-strategy#gs-6-define-and-implement-identity-and-privileged-access-strategy",
+ "DefaultValue": "By default, `Owners can manage group membership requests in My Groups` is set to `No`."
+ }
+ ]
+ },
+ {
+ "Id": "5.21",
+ "Description": "Ensure that 'Users can create Microsoft 365 groups in Azure portals, API or PowerShell' is set to 'No'",
+ "Checks": [
+ "entra_users_cannot_create_microsoft_365_groups"
+ ],
+ "Attributes": [
+ {
+ "Section": "5 Identity Services",
+ "Profile": "Level 1",
+ "AssessmentStatus": "Manual",
+ "Description": "Restrict Microsoft 365 group creation to administrators only.",
+ "RationaleStatement": "Restricting Microsoft 365 group creation to administrators only ensures that creation of Microsoft 365 groups is controlled by the administrator. Appropriate groups should be created and managed by the administrator and group creation rights should not be delegated to any other user.",
+ "ImpactStatement": "Enabling this setting could create a number of requests that would need to be managed by an administrator.",
+ "RemediationProcedure": "**Remediate from Azure Portal** 1. From Azure Home select the Portal Menu 1. Select `Microsoft Entra ID` 1. Under `Manage`, select `Groups` 1. Under `Settings`, select `General` 1. Under `Microsoft 365 Groups`, set `Users can create Microsoft 365 groups in Azure portals, API or PowerShell` to `No` 1. Click `Save`",
+ "AuditProcedure": "**Audit from Azure Portal** 1. From Azure Home select the Portal Menu 1. Select `Microsoft Entra ID` 1. Under `Manage`, select `Groups` 1. Under `Settings`, select `General` 1. Under `Microsoft 365 Groups`, ensure that `Users can create Microsoft 365 groups in Azure portals, API or PowerShell` is set to `No`",
+ "AdditionalInformation": "",
+ "References": "https://learn.microsoft.com/en-us/microsoft-365/solutions/manage-creation-of-groups?view=o365-worldwide&redirectSourcePath=%252fen-us%252farticle%252fControl-who-can-create-Office-365-Groups-4c46c8cb-17d0-44b5-9776-005fced8e618:https://learn.microsoft.com/en-us/security/benchmark/azure/mcsb-governance-strategy#gs-6-define-and-implement-identity-and-privileged-access-strategy:https://learn.microsoft.com/en-us/security/benchmark/azure/mcsb-governance-strategy#gs-2-define-and-implement-enterprise-segmentationseparation-of-duties-strategy:https://learn.microsoft.com/en-us/security/benchmark/azure/mcsb-privileged-access#pa-1-separate-and-limit-highly-privilegedadministrative-users:https://learn.microsoft.com/en-us/security/benchmark/azure/mcsb-privileged-access#pa-3-manage-lifecycle-of-identities-and-entitlements",
+ "DefaultValue": "By default, `Users can create Microsoft 365 groups in Azure portals, API or PowerShell` is set to `Yes`."
+ }
+ ]
+ },
+ {
+ "Id": "5.22",
+ "Description": "Ensure that 'Require Multifactor Authentication to register or join devices with Microsoft Entra' is set to 'Yes'",
+ "Checks": [],
+ "Attributes": [
+ {
+ "Section": "5 Identity Services",
+ "Profile": "Level 1",
+ "AssessmentStatus": "Manual",
+ "Description": "**NOTE:** This recommendation is only relevant if your subscription is using Per-User MFA. If your organization is licensed to use Conditional Access, the preferred method of requiring MFA to join devices to Entra ID is to use a Conditional Access policy (see additional information below for link). Joining or registering devices to Microsoft Entra ID should require multi-factor authentication.",
+ "RationaleStatement": "Multi-factor authentication is recommended when adding devices to Microsoft Entra ID. When set to `Yes`, users who are adding devices from the internet must first use the second method of authentication before their device is successfully added to the directory. This ensures that rogue devices are not added to the domain using a compromised user account.",
+ "ImpactStatement": "A slight impact of additional overhead, as Administrators will now have to approve every access to the domain.",
+ "RemediationProcedure": "**Remediate from Azure Portal** 1. From Azure Home select the Portal Menu 1. Select `Microsoft Entra ID` 1. Under `Manage`, select `Devices` 1. Under `Manage`, select `Device settings` 1. Under `Microsoft Entra join and registration settings`, set `Require Multifactor Authentication to register or join devices with Microsoft Entra` to `Yes` 1. Click `Save`",
+ "AuditProcedure": "**Audit from Azure Portal** 1. From Azure Home select the Portal Menu 1. Select `Microsoft Entra ID` 1. Under `Manage`, select `Devices` 1. Under `Manage`, select `Device settings` 1. Under `Microsoft Entra join and registration settings`, ensure that `Require Multifactor Authentication to register or join devices with Microsoft Entra` is set to `Yes`",
+ "AdditionalInformation": "If Conditional Access is available, this recommendation should be bypassed in favor of the Conditional Access implementation of requiring Multifactor Authentication to register or join devices with Microsoft Entra. https://learn.microsoft.com/en-us/entra/identity/conditional-access/how-to-policy-mfa-device-register-join",
+ "References": "https://learn.microsoft.com/en-us/entra/identity/conditional-access/how-to-policy-mfa-device-register-join:https://learn.microsoft.com/en-us/security/benchmark/azure/mcsb-identity-management#im-6-use-strong-authentication-controls",
+ "DefaultValue": "By default, `Require Multifactor Authentication to register or join devices with Microsoft Entra` is set to `No`."
+ }
+ ]
+ },
+ {
+ "Id": "5.23",
+ "Description": "Ensure that no custom subscription administrator roles exist",
+ "Checks": [
+ "iam_subscription_roles_owner_custom_not_created"
+ ],
+ "Attributes": [
+ {
+ "Section": "5 Identity Services",
+ "Profile": "Level 1",
+ "AssessmentStatus": "Automated",
+ "Description": "The principle of least privilege should be followed and only necessary privileges should be assigned instead of allowing full administrative access.",
+ "RationaleStatement": "Custom roles in Azure with administrative access can obfuscate the permissions granted and introduce complexity and blind spots to the management of privileged identities. For less mature security programs without regular identity audits, the creation of Custom roles should be avoided entirely. For more mature security programs with regular identity audits, Custom Roles should be audited for use and assignment, used minimally, and the principle of least privilege should be observed when granting permissions",
+ "ImpactStatement": "Subscriptions will need to be handled by Administrators with permissions.",
+ "RemediationProcedure": "**Remediate from Azure Portal** 1. From Azure Home select the Portal Menu. 1. Select `Subscriptions`. 1. Select a subscription. 1. Select `Access control (IAM)`. 1. Select `Roles`. 1. Click `Type` and select `Custom role` from the drop-down menu. 1. Check the box next to each role which grants subscription administrator privileges. 1. Select `Delete`. 1. Select `Yes`. **Remediate from Azure CLI** List custom roles: ``` az role definition list --custom-role-only True ``` Check for entries with `assignableScope` of the `subscription`, and an action of `*`. To remove a violating role: ``` az role definition delete --name ``` Note that any role assignments must be removed before a custom role can be deleted. Ensure impact is assessed before deleting a custom role granting subscription administrator privileges.",
+ "AuditProcedure": "**Audit from Azure Portal** 1. From Azure Home select the Portal Menu. 1. Select `Subscriptions`. 1. Select a subscription. 1. Select `Access control (IAM)`. 1. Select `Roles`. 1. Click `Type` and select `Custom role` from the drop-down menu. 1. Select `View` next to a role. 1. Select `JSON`. 1. Check for `assignableScopes` set to the subscription, and `actions` set to `*`. 1. Repeat steps 7-9 for each custom role. **Audit from Azure CLI** List custom roles: ``` az role definition list --custom-role-only True ``` Check for entries with `assignableScope` of the `subscription`, and an action of `*` **Audit from PowerShell** ``` Connect-AzAccount Get-AzRoleDefinition |Where-Object {($_.IsCustom -eq $true) -and ($_.Actions.contains('*'))} ``` Check the output for `AssignableScopes` value set to the subscription. **Audit from Azure Policy** If referencing a digital copy of this Benchmark, clicking a Policy ID will open a link to the associated Policy definition in Azure. If referencing a printed copy, you can search Policy IDs from this URL: https://portal.azure.com/#view/Microsoft_Azure_Policy/PolicyMenuBlade/~/Definitions - **Policy ID:** [a451c1ef-c6ca-483d-87ed-f49761e3ffb5](https://portal.azure.com/#view/Microsoft_Azure_Policy/PolicyDetailBlade/definitionId/%2Fproviders%2FMicrosoft.Authorization%2FpolicyDefinitions%2Fa451c1ef-c6ca-483d-87ed-f49761e3ffb5) **- Name:** 'Audit usage of custom RBAC roles'",
+ "AdditionalInformation": "",
+ "References": "https://docs.microsoft.com/en-us/azure/billing/billing-add-change-azure-subscription-administrator:https://learn.microsoft.com/en-us/security/benchmark/azure/mcsb-privileged-access#pa-1-separate-and-limit-highly-privilegedadministrative-users:https://learn.microsoft.com/en-us/security/benchmark/azure/mcsb-privileged-access#pa-3-manage-lifecycle-of-identities-and-entitlements:https://learn.microsoft.com/en-us/security/benchmark/azure/mcsb-governance-strategy#gs-2-define-and-implement-enterprise-segmentationseparation-of-duties-strategy:https://learn.microsoft.com/en-us/security/benchmark/azure/mcsb-governance-strategy#gs-6-define-and-implement-identity-and-privileged-access-strategy:https://learn.microsoft.com/en-us/security/benchmark/azure/mcsb-privileged-access#pa-7-follow-just-enough-administration-least-privilege-principle",
+ "DefaultValue": "By default, no custom owner roles are created."
+ }
+ ]
+ },
+ {
+ "Id": "5.24",
+ "Description": "Ensure that a custom role is assigned permissions for administering resource locks",
+ "Checks": [
+ "iam_custom_role_has_permissions_to_administer_resource_locks"
+ ],
+ "Attributes": [
+ {
+ "Section": "5 Identity Services",
+ "Profile": "Level 1",
+ "AssessmentStatus": "Manual",
+ "Description": "Resource locking is a powerful protection mechanism that can prevent inadvertent modification or deletion of resources within Azure subscriptions and resource groups, and it is a recommended NIST configuration.",
+ "RationaleStatement": "Given that the resource lock functionality is outside of standard Role-Based Access Control (RBAC), it would be prudent to create a resource lock administrator role to prevent inadvertent unlocking of resources.",
+ "ImpactStatement": "By adding this role, specific permissions may be granted for managing only resource locks rather than needing to provide the broad Owner or User Access Administrator role, reducing the risk of the user being able to cause unintentional damage.",
+ "RemediationProcedure": "**Remediate from Azure Portal** 1. In the Azure portal, navigate to a subscription or resource group. 1. Click `Access control (IAM)`. 1. Click `+ Add`. 1. Click `Add custom role`. 1. In the `Custom role name` field enter `Resource Lock Administrator`. 1. In the `Description` field enter `Can Administer Resource Locks`. 1. For `Baseline permissions` select `Start from scratch`. 1. Click `Next`. 1. Click `Add permissions`. 1. In the `Search for a permission` box, type `Microsoft.Authorization/locks`. 1. Click the result. 1. Check the box next to `Permission`. 1. Click `Add`. 1. Click `Review + create`. 1. Click `Create`. 1. Click `OK`. 1. Click `+ Add`. 1. Click `Add role assignment`. 1. In the `Search by role name, description, permission, or ID` box, type `Resource Lock Administrator`. 1. Select the role. 1. Click `Next`. 1. Click `+ Select members`. 1. Select appropriate members. 1. Click `Select`. 1. Click `Review + assign`. 1. Click `Review + assign` again. 1. Repeat steps 1-26 for each subscription or resource group requiring remediation. **Remediate from PowerShell:** Below is a PowerShell definition for a resource lock administrator role created at an Azure Management group level ``` Import-Module Az.Accounts Connect-AzAccount $role = Get-AzRoleDefinition User Access Administrator $role.Id = $null $role.Name = Resource Lock Administrator $role.Description = Can Administer Resource Locks $role.Actions.Clear() $role.Actions.Add(Microsoft.Authorization/locks/*) $role.AssignableScopes.Clear() * Scope at the Management group level Management group $role.AssignableScopes.Add(/providers/Microsoft.Management/managementGroups/MG-Name) New-AzRoleDefinition -Role $role Get-AzureRmRoleDefinition Resource Lock Administrator ```",
+ "AuditProcedure": "**Audit from Azure Portal** 1. In the Azure portal, navigate to a subscription or resource group. 1. Click `Access control (IAM)`. 1. Click `Roles`. 1. Click `Type : All`. 1. Click to view the drop-down menu. 1. Select `Custom role`. 1. Click `View` in the `Details` column of a custom role. 1. Review the role permissions. 1. Click `Assignments` and review the assignments. 1. Click the `X` to exit the custom role details page. 1. Repeat steps 7-10. Ensure that at least one custom role exists that assigns the `Microsoft.Authorization/locks` permission to appropriate members. 1. Repeat steps 1-11 for each subscription or resource group.",
+ "AdditionalInformation": "",
+ "References": "https://docs.microsoft.com/en-us/azure/role-based-access-control/custom-roles:https://docs.microsoft.com/en-us/azure/role-based-access-control/check-access:https://learn.microsoft.com/en-us/security/benchmark/azure/mcsb-privileged-access#pa-1-separate-and-limit-highly-privilegedadministrative-users:https://learn.microsoft.com/en-us/security/benchmark/azure/mcsb-privileged-access#pa-7-follow-just-enough-administration-least-privilege-principle:https://learn.microsoft.com/en-us/security/benchmark/azure/mcsb-privileged-access#pa-3-manage-lifecycle-of-identities-and-entitlements:https://learn.microsoft.com/en-us/security/benchmark/azure/mcsb-governance-strategy#gs-2-define-and-implement-enterprise-segmentationseparation-of-duties-strategy:https://learn.microsoft.com/en-us/security/benchmark/azure/mcsb-governance-strategy#gs-6-define-and-implement-identity-and-privileged-access-strategy",
+ "DefaultValue": "A role for administering resource locks does not exist by default."
+ }
+ ]
+ },
+ {
+ "Id": "5.25",
+ "Description": "Ensure that 'Subscription leaving Microsoft Entra tenant' and 'Subscription entering Microsoft Entra tenant' is set to 'Permit no one'",
+ "Checks": [],
+ "Attributes": [
+ {
+ "Section": "5 Identity Services",
+ "Profile": "Level 1",
+ "AssessmentStatus": "Manual",
+ "Description": "Users who are set as subscription owners are able to make administrative changes to the subscriptions and move them into and out of Microsoft Entra ID.",
+ "RationaleStatement": "Permissions to move subscriptions in and out of a Microsoft Entra tenant must only be given to appropriate administrative personnel. A subscription that is moved into a Microsoft Entra tenant may be within a folder to which other users have elevated permissions. This prevents loss of data or unapproved changes of the objects within by potential bad actors.",
+ "ImpactStatement": "Subscriptions will need to have these settings turned off to be moved.",
+ "RemediationProcedure": "**Remediate from Azure Portal** 1. From the Azure Portal Home select the portal menu 1. Select `Subscriptions` 1. In the `Advanced options` drop-down menu, select `Manage Policies` 1. Set `Subscription leaving Microsoft Entra tenant` and `Subscription entering Microsoft Entra tenant` to `Permit no one` 1. Click `Save changes`",
+ "AuditProcedure": "**Audit from Azure Portal** 1. From the Azure Portal Home select the portal menu 1. Select `Subscriptions` 1. In the `Advanced options` drop-down menu, select `Manage Policies` 1. Ensure `Subscription leaving Microsoft Entra tenant` and `Subscription entering Microsoft Entra tenant` are set to `Permit no one`",
+ "AdditionalInformation": "",
+ "References": "https://docs.microsoft.com/en-us/azure/cost-management-billing/manage/manage-azure-subscription-policy:https://learn.microsoft.com/en-us/entra/fundamentals/how-subscriptions-associated-directory:https://learn.microsoft.com/en-us/security/benchmark/azure/mcsb-identity-management#im-2-protect-identity-and-authentication-systems",
+ "DefaultValue": "By default `Subscription leaving Microsoft Entra tenant` and `Subscription entering Microsoft Entra tenant` are set to `Allow everyone (default)`"
+ }
+ ]
+ },
+ {
+ "Id": "5.26",
+ "Description": "Ensure fewer than 5 users have global administrator assignment",
+ "Checks": [
+ "entra_global_admin_in_less_than_five_users"
+ ],
+ "Attributes": [
+ {
+ "Section": "5 Identity Services",
+ "Profile": "Level 1",
+ "AssessmentStatus": "Manual",
+ "Description": "This recommendation aims to maintain a balance between security and operational efficiency by ensuring that a minimum of 2 and a maximum of 4 users are assigned the Global Administrator role in Microsoft Entra ID. Having at least two Global Administrators ensures redundancy, while limiting the number to four reduces the risk of excessive privileged access.",
+ "RationaleStatement": "The Global Administrator role has extensive privileges across all services in Microsoft Entra ID. The Global Administrator role should never be used in regular daily activities; administrators should have a regular user account for daily activities, and a separate account for administrative responsibilities. Limiting the number of Global Administrators helps mitigate the risk of unauthorized access, reduces the potential impact of human error, and aligns with the principle of least privilege to reduce the attack surface of an Azure tenant. Conversely, having at least two Global Administrators ensures that administrative functions can be performed without interruption in case of unavailability of a single admin.",
+ "ImpactStatement": "Implementing this recommendation may require changes in administrative workflows or the redistribution of roles and responsibilities. Adequate training and awareness should be provided to all Global Administrators.",
+ "RemediationProcedure": "**Remediate from Azure Portal** 1. From Azure Home select the Portal Menu 1. Select `Microsoft Entra ID` 1. Under `Manage`, select `Roles and administrators` 1. Under `Administrative Roles`, select `Global Administrator` If more than 4 users are assigned: 1. Remove Global Administrator role for users which do not or no longer require the role. 1. Assign Global Administrator role via PIM which can be activated when required. 1. Assign more granular roles to users to conduct their duties. If only one user is assigned: 1. Provide the Global Administrator role to a trusted user or create a break glass admin account.",
+ "AuditProcedure": "**Audit from Azure Portal** 1. From Azure Home select the Portal Menu 1. Select `Microsoft Entra ID` 1. Under `Manage`, select `Roles and administrators` 1. Under `Administrative Roles`, select `Global Administrator` 1. Ensure less than 5 users are actively assigned the role. 1. Ensure that at least 2 users are actively assigned the role.",
+ "AdditionalInformation": "",
+ "References": "https://learn.microsoft.com/en-us/entra/identity/role-based-access-control/best-practices#5-limit-the-number-of-global-administrators-to-less-than-5:https://learn.microsoft.com/en-us/microsoft-365/admin/add-users/about-admin-roles?view=o365-worldwide#security-guidelines-for-assigning-roles:https://learn.microsoft.com/en-us/entra/identity/role-based-access-control/security-emergency-access:https://learn.microsoft.com/en-us/security/benchmark/azure/mcsb-privileged-access#pa-1-separate-and-limit-highly-privilegedadministrative-users",
+ "DefaultValue": ""
+ }
+ ]
+ },
+ {
+ "Id": "5.27",
+ "Description": "Ensure there are between 2 and 3 subscription owners",
+ "Checks": [],
+ "Attributes": [
+ {
+ "Section": "5 Identity Services",
+ "Profile": "Level 1",
+ "AssessmentStatus": "Automated",
+ "Description": "The Owner role in Azure grants full control over all resources in a subscription, including the ability to assign roles to others. Limit the number of security principals assigned the Owner role to between 2 and 3.",
+ "RationaleStatement": "If groups are used, ensure their membership is tightly controlled and regularly reviewed to avoid privilege sprawl. This includes user accounts, Entra ID groups, service principals, and managed identities.",
+ "ImpactStatement": "Implementation may require changes in administrative workflows or the redistribution of roles and responsibilities.",
+ "RemediationProcedure": "**Remediate from Azure Portal** 1. Go to `Subscriptions`. 2. Click the name of a subscription. 3. Click `Access Controls (IAM)`. 4. Click `Role assignments`. 5. Click `Role : All`. 6. Click `Owner`. 7. Check the box next to members from whom the owner role should be removed. 8. Click `Delete`. 9. Click `Yes`. 10. Repeat for each subscription requiring remediation. **Remediate from Azure CLI** ``` az role assignment delete --ids ```",
+ "AuditProcedure": "**Audit from Azure Portal** 1. Go to `Subscriptions`. 2. Click the name of a subscription. 3. Click `Access Controls (IAM)`. 4. Click `Role assignments`. 5. Click `Role : All`. 6. Click the arrow next to `All`. 7. Click `Owner`. 8. Ensure a minimum of 2 and a maximum of 3 members are returned. 9. Repeat steps 1-8 for each subscription. **Audit from Azure CLI** ``` az role assignment list --role Owner --scope /subscriptions/ --query \"[].{PrincipalName:principalName, Type:principalType}\" ``` Ensure a minimum of 2 and a maximum of 3 members are returned. **Audit from PowerShell** ``` Get-AzRoleAssignment -RoleDefinitionName Owner -Scope /subscriptions/ ``` **Audit from Azure Policy** - **Policy ID:** [09024ccc-0c5f-475e-9457-b7c0d9ed487b] - Name: 'There should be more than one owner assigned to your subscription' - **Policy ID:** [4f11b553-d42e-4e3a-89be-32ca364cad4c] - Name: 'A maximum of 3 owners should be designated for your subscription'",
+ "AdditionalInformation": "",
+ "References": "https://learn.microsoft.com/en-us/cli/azure/role/assignment:https://learn.microsoft.com/en-us/powershell/module/az.resources/get-azroleassignment:https://learn.microsoft.com/en-us/azure/role-based-access-control/built-in-roles/privileged#owner",
+ "DefaultValue": "A subscription has 1 owner by default."
+ }
+ ]
+ },
+ {
+ "Id": "5.28",
+ "Description": "Ensure passwordless authentication methods are considered",
+ "Checks": [],
+ "Attributes": [
+ {
+ "Section": "5 Identity Services",
+ "Profile": "Level 2",
+ "AssessmentStatus": "Manual",
+ "Description": "Passwordless authentication methods improve security and user experience by replacing passwords with something you have (e.g., a hardware key), something you are (biometrics), or something you know. Microsoft Entra ID supports Windows Hello for Business, Platform Credential for macOS, Microsoft Authenticator, Passkeys (FIDO2), and Certificate-based authentication.",
+ "RationaleStatement": "Using passwordless authentication makes sign-in easier and more secure by removing passwords, helping to protect organizations from attacks and improving the user experience.",
+ "ImpactStatement": "Implementing passwordless authentication requires administrative effort and may incur costs for some methods. It has the potential to save time and money by improving user convenience and productivity and by reducing the need for password support.",
+ "RemediationProcedure": "1. Review the passwordless authentication method options: https://learn.microsoft.com/en-us/entra/identity/authentication/concept-authentication-passwordless 2. Choose a passwordless authentication method: https://learn.microsoft.com/en-us/entra/identity/authentication/concept-authentication-passwordless#choose-a-passwordless-method 3. Implement the chosen passwordless authentication method: - Microsoft Authenticator: https://learn.microsoft.com/en-us/entra/identity/authentication/how-to-enable-authenticator-passkey - Passkeys (FIDO2): https://learn.microsoft.com/en-us/entra/identity/authentication/how-to-enable-passkey-fido2",
+ "AuditProcedure": "**Audit from Azure Portal** 1. Go to `Microsoft Entra ID`. 2. Click `Authentication methods`. 3. Under `Manage`, click `Policies`. 4. If appropriate for your organization, ensure a passwordless authentication method policy is configured.",
+ "AdditionalInformation": "",
+ "References": "https://learn.microsoft.com/en-us/entra/identity/authentication/concept-authentication-methods:https://learn.microsoft.com/en-us/entra/identity/authentication/concept-authentication-passwordless",
+ "DefaultValue": "Passwordless authentication is not enabled by default."
+ }
+ ]
+ },
+ {
+ "Id": "6.1.1.1",
+ "Description": "Ensure that a 'Diagnostic Setting' exists for Subscription Activity Logs",
+ "Checks": [
+ "monitor_diagnostic_settings_exists"
+ ],
+ "Attributes": [
+ {
+ "Section": "6 Management and Governance Services",
+ "SubSection": "6.1 Logging and Monitoring",
+ "Profile": "Level 1",
+ "AssessmentStatus": "Automated",
+ "Description": "Enable Diagnostic settings for exporting activity logs. Diagnostic settings are available for each individual resource within a subscription. Settings should be configured for all appropriate resources for your environment.",
+ "RationaleStatement": "A diagnostic setting controls how a diagnostic log is exported. By default, logs are retained only for 90 days. Diagnostic settings should be defined so that logs can be exported and stored for a longer duration to analyze security activities within an Azure subscription.",
+ "ImpactStatement": "Diagnostic settings incur costs based on the amount of data collected and the destination.",
+ "RemediationProcedure": "**Remediate from Azure Portal** To enable Diagnostic Settings on a Subscription: 1. Go to `Monitor` 2. Click on `Activity log` 3. Click on `Export Activity Logs` 4. Click `+ Add diagnostic setting` 5. Enter a `Diagnostic setting name` 6. Select `Categories` for the diagnostic setting 7. Select the appropriate `Destination details` (this may be Log Analytics, Storage Account, Event Hub, or Partner solution) 8. Click `Save` To enable Diagnostic Settings on a specific resource: 1. Go to `Monitoring` 1. Click `Diagnostic settings` 1. Select `Add diagnostic setting` 1. Enter a `Diagnostic setting name` 1. Select the appropriate log, metric, and destination (this may be Log Analytics, Storage Account, Event Hub, or Partner solution) 1. Click `Save` Repeat these step for all resources as needed. **Remediate from Azure CLI** To configure Diagnostic Settings on a Subscription: ``` az monitor diagnostic-settings subscription create --subscription --name --location <[--event-hub --event-hub-auth-rule ] [--storage-account ] [--workspace ] --logs (e.g. [{category:Security,enabled:true},{category:Administrative,enabled:true},{category:Alert,enabled:true},{category:Policy,enabled:true}]) ``` To configure Diagnostic Settings on a specific resource: ``` az monitor diagnostic-settings create --subscription --resource --name <[--event-hub --event-hub-rule ] [--storage-account ] [--workspace ] --logs --metrics ``` **Remediate from PowerShell** To configure Diagnostic Settings on a subscription: ``` $logCategories = @(); $logCategories += New-AzDiagnosticSettingSubscriptionLogSettingsObject -Category Administrative -Enabled $true $logCategories += New-AzDiagnosticSettingSubscriptionLogSettingsObject -Category Security -Enabled $true $logCategories += New-AzDiagnosticSettingSubscriptionLogSettingsObject -Category ServiceHealth -Enabled $true $logCategories += New-AzDiagnosticSettingSubscriptionLogSettingsObject -Category Alert -Enabled $true $logCategories += New-AzDiagnosticSettingSubscriptionLogSettingsObject -Category Recommendation -Enabled $true $logCategories += New-AzDiagnosticSettingSubscriptionLogSettingsObject -Category Policy -Enabled $true $logCategories += New-AzDiagnosticSettingSubscriptionLogSettingsObject -Category Autoscale -Enabled $true $logCategories += New-AzDiagnosticSettingSubscriptionLogSettingsObject -Category ResourceHealth -Enabled $true New-AzSubscriptionDiagnosticSetting -SubscriptionId -Name <[-EventHubAuthorizationRule -EventHubName ] [-StorageAccountId ] [-WorkSpaceId ] [-MarketplacePartner ID ]> -Log $logCategories ``` To configure Diagnostic Settings on a specific resource: ``` $logCategories = @() $logCategories += New-AzDiagnosticSettingLogSettingsObject -Category -Enabled $true Repeat command and variable assignment for each Log category specific to the resource where this Diagnostic Setting will get configured. $metricCategories = @() $metricCategories += New-AzDiagnosticSettingMetricSettingsObject -Enabled $true [-Category ] [-RetentionPolicyDay ] [-RetentionPolicyEnabled $true] Repeat command and variable assignment for each Metric category or use the 'AllMetrics' category. New-AzDiagnosticSetting -ResourceId -Name -Log $logCategories -Metric $metricCategories [-EventHubAuthorizationRuleId -EventHubName ] [-StorageAccountId ] [-WorkspaceId ] [-MarketplacePartnerId ]>",
+ "AuditProcedure": "**Audit from Azure Portal** To identify Diagnostic Settings on a subscription: 1. Go to `Monitor` 2. Click `Activity Log` 3. Click `Export Activity Logs` 4. Select a `Subscription` 5. Ensure a `Diagnostic setting` exists for the selected Subscription To identify Diagnostic Settings on specific resources: 1. Go to `Monitoring` 2. Click `Diagnostic settings` 3. Ensure a `Diagnostic setting` exists for all appropriate resources. **Audit from Azure CLI** To identify Diagnostic Settings on a subscription: ``` az monitor diagnostic-settings subscription list --subscription ``` To identify Diagnostic Settings on a resource ``` az monitor diagnostic-settings list --resource ``` **Audit from PowerShell** To identify Diagnostic Settings on a Subscription: ``` Get-AzDiagnosticSetting -SubscriptionId ``` To identify Diagnostic Settings on a specific resource: ``` Get-AzDiagnosticSetting -ResourceId ```",
+ "AdditionalInformation": "",
+ "References": "https://docs.microsoft.com/en-us/azure/monitoring-and-diagnostics/monitoring-overview-activity-logs#export-the-activity-log-with-a-log-profile:https://learn.microsoft.com/en-us/cli/azure/monitor/diagnostic-settings?view=azure-cli-latest:https://learn.microsoft.com/en-us/security/benchmark/azure/mcsb-logging-threat-detection#lt-3-enable-logging-for-security-investigation",
+ "DefaultValue": "By default, diagnostic setting is not set."
+ }
+ ]
+ },
+ {
+ "Id": "6.1.1.2",
+ "Description": "Ensure Diagnostic Setting captures appropriate categories",
+ "Checks": [
+ "monitor_diagnostic_setting_with_appropriate_categories"
+ ],
+ "Attributes": [
+ {
+ "Section": "6 Management and Governance Services",
+ "SubSection": "6.1 Logging and Monitoring",
+ "Profile": "Level 1",
+ "AssessmentStatus": "Automated",
+ "Description": "**Prerequisite**: A Diagnostic Setting must exist. If a Diagnostic Setting does not exist, the navigation and options within this recommendation will not be available. Please review the recommendation at the beginning of this subsection titled: Ensure that a 'Diagnostic Setting' exists. The diagnostic setting should be configured to log the appropriate activities from the control/management plane.",
+ "RationaleStatement": "A diagnostic setting controls how the diagnostic log is exported. Capturing the diagnostic setting categories for appropriate control/management plane activities allows proper alerting.",
+ "ImpactStatement": "Enabling additional categories may increase storage costs.",
+ "RemediationProcedure": "**Remediate from Azure Portal** 1. Go to `Monitor`. 1. Click `Activity log`. 1. Click on `Export Activity Logs`. 1. Select the `Subscription` from the drop down menu. 1. Click `Edit setting` next to a diagnostic setting. 1. Check the following categories: `Administrative, Alert, Policy, and Security`. 1. Choose the destination details according to your organization's needs. 1. Click `Save`. **Remediate from Azure CLI** ``` az monitor diagnostic-settings subscription create --subscription --name --location <[--event-hub --event-hub-auth-rule ] [--storage-account ] [--workspace ] --logs [{category:Security,enabled:true},{category:Administrative,enabled:true},{category:Alert,enabled:true},{category:Policy,enabled:true}] ``` **Remediate from PowerShell** ``` $logCategories = @(); $logCategories += New-AzDiagnosticSettingSubscriptionLogSettingsObject -Category Administrative -Enabled $true $logCategories += New-AzDiagnosticSettingSubscriptionLogSettingsObject -Category Security -Enabled $true $logCategories += New-AzDiagnosticSettingSubscriptionLogSettingsObject -Category Alert -Enabled $true $logCategories += New-AzDiagnosticSettingSubscriptionLogSettingsObject -Category Policy -Enabled $true New-AzSubscriptionDiagnosticSetting -SubscriptionId -Name <[-EventHubAuthorizationRule -EventHubName ] [-StorageAccountId ] [-WorkSpaceId ] [-MarketplacePartner ID ]> -Log $logCategories ```",
+ "AuditProcedure": "**Audit from Azure Portal** 1. Go to `Monitor`. 1. Click `Activity log`. 1. Click on `Export Activity Logs`. 1. Select the appropriate `Subscription`. 1. Click `Edit setting` next to a diagnostic setting. 1. Ensure that the following categories are checked: `Administrative, Alert, Policy, and Security`. **Audit from Azure CLI** Ensure the categories `'Administrative', 'Alert', 'Policy', and 'Security'` set to: 'enabled: true' ``` az monitor diagnostic-settings subscription list --subscription ``` **Audit from PowerShell** Ensure the categories Administrative, Alert, Policy, and Security are set to Enabled:True ``` Get-AzSubscriptionDiagnosticSetting -Subscription ``` **Audit from Azure Policy** If referencing a digital copy of this Benchmark, clicking a Policy ID will open a link to the associated Policy definition in Azure. If referencing a printed copy, you can search Policy IDs from this URL: https://portal.azure.com/#view/Microsoft_Azure_Policy/PolicyMenuBlade/~/Definitions - **Policy ID:** [3b980d31-7904-4bb7-8575-5665739a8052](https://portal.azure.com/#view/Microsoft_Azure_Policy/PolicyDetailBlade/definitionId/%2Fproviders%2FMicrosoft.Authorization%2FpolicyDefinitions%2F3b980d31-7904-4bb7-8575-5665739a8052) **- Name:** 'An activity log alert should exist for specific Security operations' - **Policy ID:** [b954148f-4c11-4c38-8221-be76711e194a](https://portal.azure.com/#view/Microsoft_Azure_Policy/PolicyDetailBlade/definitionId/%2Fproviders%2FMicrosoft.Authorization%2FpolicyDefinitions%2Fb954148f-4c11-4c38-8221-be76711e194a) **- Name:** 'An activity log alert should exist for specific Administrative operations' - **Policy ID:** [c5447c04-a4d7-4ba8-a263-c9ee321a6858](https://portal.azure.com/#view/Microsoft_Azure_Policy/PolicyDetailBlade/definitionId/%2Fproviders%2FMicrosoft.Authorization%2FpolicyDefinitions%2Fc5447c04-a4d7-4ba8-a263-c9ee321a6858) **- Name:** 'An activity log alert should exist for specific Policy operations'",
+ "AdditionalInformation": "",
+ "References": "https://docs.microsoft.com/en-us/azure/azure-monitor/platform/diagnostic-settings:https://docs.microsoft.com/en-us/azure/azure-monitor/samples/resource-manager-diagnostic-settings:https://learn.microsoft.com/en-us/security/benchmark/azure/mcsb-logging-threat-detection#lt-3-enable-logging-for-security-investigation:https://learn.microsoft.com/en-us/cli/azure/monitor/diagnostic-settings?view=azure-cli-latest:https://learn.microsoft.com/en-us/powershell/module/az.monitor/new-azsubscriptiondiagnosticsetting?view=azps-9.2.0",
+ "DefaultValue": "When the diagnostic setting is created using Azure Portal, by default no categories are selected."
+ }
+ ]
+ },
+ {
+ "Id": "6.1.1.3",
+ "Description": "Ensure the storage account containing the container with activity logs is encrypted with customer-managed key (CMK)",
+ "Checks": [
+ "monitor_storage_account_with_activity_logs_cmk_encrypted"
+ ],
+ "Attributes": [
+ {
+ "Section": "6 Management and Governance Services",
+ "SubSection": "6.1 Logging and Monitoring",
+ "Profile": "Level 2",
+ "AssessmentStatus": "Manual",
+ "Description": "Storage accounts with the activity log exports can be configured to use Customer Managed Keys (CMK).",
+ "RationaleStatement": "Configuring the storage account with the activity log export container to use CMKs provides additional confidentiality controls on log data, as a given user must have read permission on the corresponding storage account and must be granted decrypt permission by the CMK.",
+ "ImpactStatement": "**NOTE:** You must have your key vault setup to utilize this. All Audit Logs will be encrypted with a key you provide. You will need to set up customer managed keys separately, and you will select which key to use via the instructions here. You will be responsible for the lifecycle of the keys, and will need to manually replace them at your own determined intervals to keep the data secure.",
+ "RemediationProcedure": "**Remediate from Azure Portal** 1. Go to `Monitor`. 1. Select `Activity log`. 1. Select `Export Activity Logs`. 1. Select a `Subscription`. 1. Note the name of the `Storage Account` for the diagnostic setting. 1. Navigate to `Storage accounts`. 1. Click on the storage account. 1. Under `Security + networking`, click `Encryption`. 1. Next to `Encryption type`, select `Customer-managed keys`. 1. Complete the steps to configure a customer-managed key for encryption of the storage account. **Remediate from Azure CLI** ``` az storage account update --name --resource-group --encryption-key-source=Microsoft.Keyvault --encryption-key-vault --encryption-key-name --encryption-key-version ``` **Remediate from PowerShell** ``` Set-AzStorageAccount -ResourceGroupName -Name -KeyvaultEncryption -KeyVaultUri -KeyName ```",
+ "AuditProcedure": "**Audit from Azure Portal** 1. Go to `Monitor`. 1. Select `Activity log`. 1. Select `Export Activity Logs`. 1. Select a `Subscription`. 1. Note the name of the `Storage Account` for the diagnostic setting. 1. Navigate to `Storage accounts`. 1. Click on the storage account name noted in Step 5. 1. Under `Security + networking`, click `Encryption`. 1. Ensure `Customer-managed keys` is selected and a key is set. **Audit from Azure CLI** 1. Get storage account id configured with log profile: ``` az monitor diagnostic-settings subscription list --subscription --query 'value[*].storageAccountId' ``` 2. Ensure the storage account is encrypted with CMK: ``` az storage account list --query [?name==''] ``` In command output ensure `keySource` is set to `Microsoft.Keyvault` and `keyVaultProperties` is not set to `null` **Audit from PowerShell** ``` Get-AzStorageAccount -ResourceGroupName -Name |select-object -ExpandProperty encryption|format-list ``` Ensure the value of `KeyVaultProperties` is not `null` or empty, and ensure `KeySource` is not set to `Microsoft.Storage`. **Audit from Azure Policy** If referencing a digital copy of this Benchmark, clicking a Policy ID will open a link to the associated Policy definition in Azure. If referencing a printed copy, you can search Policy IDs from this URL: https://portal.azure.com/#view/Microsoft_Azure_Policy/PolicyMenuBlade/~/Definitions - **Policy ID:** [fbb99e8e-e444-4da0-9ff1-75c92f5a85b2](https://portal.azure.com/#view/Microsoft_Azure_Policy/PolicyDetailBlade/definitionId/%2Fproviders%2FMicrosoft.Authorization%2FpolicyDefinitions%2Ffbb99e8e-e444-4da0-9ff1-75c92f5a85b2) **- Name:** 'Storage account containing the container with activity logs must be encrypted with BYOK'",
+ "AdditionalInformation": "",
+ "References": "https://learn.microsoft.com/en-us/security/benchmark/azure/mcsb-data-protection#dp-5-use-customer-managed-key-option-in-data-at-rest-encryption-when-required:https://learn.microsoft.com/en-us/azure/azure-monitor/essentials/activity-log?tabs=cli#managing-legacy-log-profiles",
+ "DefaultValue": "By default, for a storage account `keySource` is set to `Microsoft.Storage` allowing encryption with vendor Managed key and not a Customer Managed Key."
+ }
+ ]
+ },
+ {
+ "Id": "6.1.1.4",
+ "Description": "Ensure that logging for Azure Key Vault is 'Enabled'",
+ "Checks": [
+ "keyvault_logging_enabled"
+ ],
+ "Attributes": [
+ {
+ "Section": "6 Management and Governance Services",
+ "SubSection": "6.1 Logging and Monitoring",
+ "Profile": "Level 1",
+ "AssessmentStatus": "Automated",
+ "Description": "Enable AuditEvent logging for key vault instances to ensure interactions with key vaults are logged and available.",
+ "RationaleStatement": "Monitoring how and when key vaults are accessed, and by whom, enables an audit trail of interactions with confidential information, keys, and certificates managed by Azure Key Vault. Enabling logging for Key Vault saves information in a user provided destination of either an Azure storage account or Log Analytics workspace. The same destination can be used for collecting logs for multiple Key Vaults.",
+ "ImpactStatement": "Enabling logging incurs costs based on the volume of logs generated.",
+ "RemediationProcedure": "**Remediate from Azure Portal** 1. Go to `Key vaults`. 2. Select a Key vault. 3. Under `Monitoring`, select `Diagnostic settings`. 4. Click `Edit setting` to update an existing diagnostic setting, or `Add diagnostic setting` to create a new one. 5. If creating a new diagnostic setting, provide a name. 6. Configure an appropriate destination. 7. Under `Category groups`, check `audit` and `allLogs`. 8. Click `Save`. **Remediate from Azure CLI** To update an existing `Diagnostic Settings` ``` az monitor diagnostic-settings update --name --resource ``` To create a new `Diagnostic Settings` ``` az monitor diagnostic-settings create --name --resource --logs [{category:audit,enabled:true},{category:allLogs,enabled:true}] --metrics [{category:AllMetrics,enabled:true}] <[--event-hub --event-hub-rule