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. ![Architecture](docs/img/architecture.png) +# 🤖 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). + +Prowler Lighthouse Architecture +Prowler Lighthouse Architecture + +### 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 architecture - -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 + +![Get Account ID](/images/providers/alibaba-account-id.png) + +### 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" + + ![Cloud Providers Page](/images/prowler-app/cloud-providers-page.png) + +3. Click "Add Cloud Provider" + + ![Add a Cloud Provider](/images/prowler-app/add-cloud-provider.png) + +4. Select "Alibaba Cloud" + + ![Select Alibaba Cloud](/images/providers/select-alibaba-cloud.png) + +5. Enter your Alibaba Cloud Account ID and optionally provide a friendly alias + + ![Add Account ID](/images/providers/add-alibaba-account-id.png) + +### Step 3: Choose and Provide Authentication + +After the Account ID is in place, select the authentication method that matches your Alibaba Cloud setup: + +![Select Auth Method](/images/providers/select-auth-method-alibaba.png) + +#### 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 + + ![Input the Role ARN](/images/providers/alibaba-get-role-arn.png) + + +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** + + ![Filled Credentials Page](/images/providers/alibaba-credentials-form.png) -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 + + ![Launch Scan](/images/providers/launch-scan-alibaba.png) + +--- + +## 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. -Lighthouse AI Architecture +Prowler Lighthouse Architecture +Prowler Lighthouse Architecture + -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 | --storage-account |--workspace | --marketplace-partner-id ]> ``` **Remediate from PowerShell** Create the `Log` settings object ``` $logSettings = @() $logSettings += New-AzDiagnosticSettingLogSettingsObject -Enabled $true -Category audit $logSettings += New-AzDiagnosticSettingLogSettingsObject -Enabled $true -Category allLogs ``` Create the `Metric` settings object ``` $metricSettings = @() $metricSettings += New-AzDiagnosticSettingMetricSettingsObject -Enabled $true -Category AllMetrics ``` Create the `Diagnostic Settings` for each `Key Vault` ``` New-AzDiagnosticSetting -Name -ResourceId -Log $logSettings -Metric $metricSettings [-StorageAccountId | -EventHubName -EventHubAuthorizationRuleId | -WorkSpaceId | -MarketPlacePartnerId ] ```", + "AuditProcedure": "**Audit from Azure Portal** 1. Go to `Key vaults`. 1. For each Key vault, under `Monitoring`, go to `Diagnostic settings`. 1. Click `Edit setting` next to a diagnostic setting. 1. Ensure that a destination is configured. 1. Under `Category groups`, ensure that `audit` and `allLogs` are checked. **Audit from Azure CLI** List all key vaults ``` az keyvault list ``` For each keyvault `id` ``` az monitor diagnostic-settings list --resource ``` Ensure that `storageAccountId` reflects your desired destination and that `categoryGroup` and `enabled` are set as follows in the sample outputs below. ``` logs: [ { categoryGroup: audit, enabled: true, }, { categoryGroup: allLogs, enabled: true, } ``` **Audit from PowerShell** List the key vault(s) in the subscription ``` Get-AzKeyVault ``` For each key vault, run the following: ``` Get-AzDiagnosticSetting -ResourceId ``` Ensure that `StorageAccountId`, `ServiceBusRuleId`, `MarketplacePartnerId`, or `WorkspaceId` is set as appropriate. Also, ensure that `enabled` is set to `true`, and that `categoryGroup` reflects both `audit` and `allLogs` category groups. **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:** [cf820ca0-f99e-4f3e-84fb-66e913812d21](https://portal.azure.com/#view/Microsoft_Azure_Policy/PolicyDetailBlade/definitionId/%2Fproviders%2FMicrosoft.Authorization%2FpolicyDefinitions%2Fcf820ca0-f99e-4f3e-84fb-66e913812d21) **- Name:** 'Resource logs in Key Vault should be enabled'", + "AdditionalInformation": "**DEPRECATION WARNING** Retention rules for Key Vault logging is being migrated to Azure Storage Lifecycle Management. Retention rules should be set based on the needs of your organization and security or compliance frameworks. Please visit [https://learn.microsoft.com/en-us/azure/azure-monitor/essentials/migrate-to-azure-storage-lifecycle-policy?tabs=portal](https://learn.microsoft.com/en-us/azure/azure-monitor/essentials/migrate-to-azure-storage-lifecycle-policy?tabs=portal) for detail on migrating your retention rules. Microsoft has provided the following deprecation timeline: March 31, 2023 – The Diagnostic Settings Storage Retention feature will no longer be available to configure new retention rules for log data. This includes using the portal, CLI PowerShell, and ARM and Bicep templates. If you have configured retention settings, you'll still be able to see and change them in the portal. March 31, 2024 – You will no longer be able to use the API (CLI, Powershell, or templates), or Azure portal to configure retention setting unless you're changing them to 0. Existing retention rules will still be respected. September 30, 2025 – All retention functionality for the Diagnostic Settings Storage Retention feature will be disabled across all environments.", + "References": "https://docs.microsoft.com/en-us/azure/key-vault/general/howto-logging:https://learn.microsoft.com/en-us/security/benchmark/azure/mcsb-data-protection#dp-8-ensure-security-of-key-and-certificate-repository:https://learn.microsoft.com/en-us/security/benchmark/azure/mcsb-logging-threat-detection#lt-3-enable-logging-for-security-investigation", + "DefaultValue": "By default, Diagnostic AuditEvent logging is not enabled for Key Vault instances." + } + ] + }, + { + "Id": "6.1.1.5", + "Description": "Ensure that Network Security Group Flow logs are captured and sent to Log Analytics", + "Checks": [ + "network_flow_log_captured_sent" + ], + "Attributes": [ + { + "Section": "6 Management and Governance Services", + "SubSection": "6.1 Logging and Monitoring", + "Profile": "Level 2", + "AssessmentStatus": "Manual", + "Description": "Ensure that network flow logs are captured and fed into a central log analytics workspace. **Retirement Notice** On September 30, 2027, network security group (NSG) flow logs will be retired. Starting June 30, 2025, it will no longer be possible to create new NSG flow logs. Azure recommends migrating to virtual network flow logs. Review https://azure.microsoft.com/en-gb/updates?id=Azure-NSG-flow-logs-Retirement for more information. For virtual network flow logs, consider applying the recommendation `Ensure that virtual network flow logs are captured and sent to Log Analytics` in this section.", + "RationaleStatement": "Network Flow Logs provide valuable insight into the flow of traffic around your network and feed into both Azure Monitor and Azure Sentinel (if in use), permitting the generation of visual flow diagrams to aid with analyzing for lateral movement, etc.", + "ImpactStatement": "The impact of configuring NSG Flow logs is primarily one of cost and configuration. If deployed, it will create storage accounts that hold minimal amounts of data on a 5-day lifecycle before feeding to Log Analytics Workspace. This will increase the amount of data stored and used by Azure Monitor.", + "RemediationProcedure": "**Remediate from Azure Portal** 1. Navigate to `Network Watcher`. 1. Under `Logs`, select `Flow logs`. 1. Select `+ Create`. 1. Select the desired Subscription. 1. For `Flow log type`, select `Network security group`. 1. Select `+ Select target resource`. 1. Select `Network security group`. 1. Select a network security group. 1. Click `Confirm selection`. 1. Select or create a new Storage Account. 1. If using a v2 storage account, input the retention in days to retain the log. 1. Click `Next`. 1. Under `Analytics`, for `Flow log version`, select `Version 2`. 1. Check the box next to `Enable traffic analytics`. 1. Select a processing interval. 1. Select a `Log Analytics Workspace`. 1. Select `Next`. 1. Optionally add Tags. 1. Select `Review + create`. 1. Select `Create`. ***Warning*** The remediation policy creates remediation deployment and names them by concatenating the subscription name and the resource group name. The MAXIMUM permitted length of a deployment name is 64 characters. Exceeding this will cause the remediation task to fail.", + "AuditProcedure": "**Audit from Azure Portal** 1. Navigate to `Network Watcher`. 1. Under `Logs`, select `Flow logs`. 1. Click `Add filter`. 1. From the `Filter` drop-down, select `Flow log type`. 1. From the `Value` drop-down, check `Network security group` only. 1. Click `Apply`. 1. Ensure that at least one network security group flow log is listed and is configured to send logs to a `Log Analytics Workspace`. **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:** [27960feb-a23c-4577-8d36-ef8b5f35e0be](https://portal.azure.com/#view/Microsoft_Azure_Policy/PolicyDetailBlade/definitionId/%2Fproviders%2FMicrosoft.Authorization%2FpolicyDefinitions%2F27960feb-a23c-4577-8d36-ef8b5f35e0be) **- Name:** 'All flow log resources should be in enabled state' - **Policy ID:** [c251913d-7d24-4958-af87-478ed3b9ba41](https://portal.azure.com/#view/Microsoft_Azure_Policy/PolicyDetailBlade/definitionId/%2Fproviders%2FMicrosoft.Authorization%2FpolicyDefinitions%2Fc251913d-7d24-4958-af87-478ed3b9ba41) **- Name:** 'Flow logs should be configured for every network security group' - **Policy ID:** [4c3c6c5f-0d47-4402-99b8-aa543dd8bcee](https://portal.azure.com/#view/Microsoft_Azure_Policy/PolicyDetailBlade/definitionId/%2Fproviders%2FMicrosoft.Authorization%2FpolicyDefinitions%2F4c3c6c5f-0d47-4402-99b8-aa543dd8bcee) **- Name:** 'Flow logs should be configured for every virtual network'", + "AdditionalInformation": "", + "References": "https://docs.microsoft.com/en-us/azure/network-watcher/network-watcher-nsg-flow-logging-portal:https://learn.microsoft.com/en-us/security/benchmark/azure/mcsb-logging-threat-detection#lt-4-enable-network-logging-for-security-investigation", + "DefaultValue": "By default Network Security Group logs are not sent to Log Analytics." + } + ] + }, + { + "Id": "6.1.1.6", + "Description": "Ensure that logging for Azure AppService 'HTTP logs' is enabled", + "Checks": [ + "app_http_logs_enabled" + ], + "Attributes": [ + { + "Section": "6 Management and Governance Services", + "SubSection": "6.1 Logging and Monitoring", + "Profile": "Level 1", + "AssessmentStatus": "Automated", + "Description": "Enable AppServiceHTTPLogs diagnostic log category for Azure App Service instances to ensure all http requests are captured and centrally logged.", + "RationaleStatement": "Capturing web requests can be important supporting information for security analysts performing monitoring and incident response activities. Once logging, these logs can be ingested into SIEM or other central aggregation point for the organization.", + "ImpactStatement": "Log consumption and processing will incur additional cost.", + "RemediationProcedure": "**Remediate from Azure Portal** 1. Go to `App Services`. For each `App Service`: 2. Under `Monitoring`, go to `Diagnostic settings`. 3. To update an existing diagnostic setting, click `Edit setting` against the setting. To create a new diagnostic setting, click `Add diagnostic setting` and provide a name for the new setting. 4. Check the checkbox next to `HTTP logs`. 5. Configure a destination based on your specific logging consumption capability (for example Stream to an event hub and then consuming with SIEM integration for Event Hub logging). 6. Click `Save`.", + "AuditProcedure": "**Audit from Azure Portal** 1. Go to `App Services`. For each `App Service`: 2. Under `Monitoring`, go to `Diagnostic settings`. 3. Ensure a diagnostic setting exists that logs `HTTP logs` to a destination aligned to your environment's approach to log consumption (event hub, storage account, etc. dependent on what is consuming the logs such as SIEM or other log aggregation utility). **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:** [91a78b24-f231-4a8a-8da9-02c35b2b6510](https://portal.azure.com/#view/Microsoft_Azure_Policy/PolicyDetailBlade/definitionId/%2Fproviders%2FMicrosoft.Authorization%2FpolicyDefinitions%2F91a78b24-f231-4a8a-8da9-02c35b2b6510) **- Name:** 'App Service apps should have resource logs enabled' - **Policy ID:** [d639b3af-a535-4bef-8dcf-15078cddf5e2](https://portal.azure.com/#view/Microsoft_Azure_Policy/PolicyDetailBlade/definitionId/%2Fproviders%2FMicrosoft.Authorization%2FpolicyDefinitions%2Fd639b3af-a535-4bef-8dcf-15078cddf5e2) **- Name:** 'App Service app slots should have resource logs enabled'", + "AdditionalInformation": "", + "References": "https://docs.microsoft.com/en-us/azure/app-service/troubleshoot-diagnostic-logs:https://learn.microsoft.com/en-us/security/benchmark/azure/mcsb-logging-threat-detection#lt-3-enable-logging-for-security-investigation", + "DefaultValue": "Not configured." + } + ] + }, + { + "Id": "6.1.1.7", + "Description": "Ensure that virtual network flow logs are captured and sent to Log Analytics", + "Checks": [ + "network_flow_log_captured_sent" + ], + "Attributes": [ + { + "Section": "6 Management and Governance Services", + "SubSection": "6.1 Logging and Monitoring", + "Profile": "Level 2", + "AssessmentStatus": "Manual", + "Description": "Ensure that virtual network flow logs are captured and fed into a central log analytics workspace.", + "RationaleStatement": "Virtual network flow logs provide critical visibility into traffic patterns. Sending logs to a Log Analytics workspace enables centralized analysis, correlation, and alerting for faster threat detection and response.", + "ImpactStatement": "* Virtual network flow logs are charged per gigabyte of network flow logs collected and come with a free tier of 5 GB/month per subscription. * If traffic analytics is enabled with virtual network flow logs, traffic analytics pricing applies at per gigabyte processing rates. * The storage of logs is charged separately.", + "RemediationProcedure": "**Remediate from Azure Portal** 1. Go to `Network Watcher`. 1. Under `Logs`, click `Flow logs`. 1. Click `+ Create`. 1. Select a subscription. 1. Next to `Flow log type`, select `Virtual network`. 1. Click `+ Select target resource`. 1. Select `Virtual network`. 1. Select a virtual network. 1. Click `Confirm selection`. 1. Select a storage account, or create a new storage account. 1. Set the retention in days for the storage account. 1. Click `Next`. 1. Under `Analytics`, for `Flow logs version`, select `Version 2`. 1. Check the box next to `Enable traffic analytics`. 1. Select a processing interval. 1. Select a `Log Analytics Workspace`. 1. Click `Next`. 1. Optionally, add `Tags`. 1. Click `Review + create`. 1. Click `Create`. 1. Repeat steps 1-20 for each subscription or virtual network requiring remediation.", + "AuditProcedure": "**Audit from Azure Portal** 1. Go to `Network Watcher`. 1. Under `Logs`, select `Flow logs`. 1. Click `Add filter`. 1. From the `Filter` drop-down menu, select `Flow log type`. 1. From the `Value` drop-down menu, check `Virtual network` only. 1. Click `Apply`. 1. Ensure that at least one virtual network flow log is listed and is configured to send logs to a `Log Analytics Workspace`. **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:** [2f080164-9f4d-497e-9db6-416dc9f7b48a](https://portal.azure.com/#view/Microsoft_Azure_Policy/PolicyDetailBlade/definitionId/%2Fproviders%2FMicrosoft.Authorization%2FpolicyDefinitions%2F2f080164-9f4d-497e-9db6-416dc9f7b48a) **- Name:** 'Network Watcher flow logs should have traffic analytics enabled' - **Policy ID:** [4c3c6c5f-0d47-4402-99b8-aa543dd8bcee](https://portal.azure.com/#view/Microsoft_Azure_Policy/PolicyDetailBlade/definitionId/%2Fproviders%2FMicrosoft.Authorization%2FpolicyDefinitions%2F4c3c6c5f-0d47-4402-99b8-aa543dd8bcee) **- Name:** 'Audit flow logs configuration for every virtual network'", + "AdditionalInformation": "On September 30, 2027, NSG flow logs will be retired, and creating new NSG flow logs will no longer be possible after June 30, 2025. Azure recommends migrating to virtual network flow logs, which address NSG flow log limitations. After retirement, traffic analytics using NSG flow logs will no longer be supported, and existing NSG flow log resources will be deleted. Previously collected NSG flow log records will remain available per their retention policies. For details, see the official announcement: https://azure.microsoft.com/en-gb/updates?id=Azure-NSG-flow-logs-Retirement.", + "References": "https://learn.microsoft.com/en-us/azure/network-watcher/vnet-flow-logs-overview:https://learn.microsoft.com/en-us/azure/network-watcher/vnet-flow-logs-cli", + "DefaultValue": "" + } + ] + }, + { + "Id": "6.1.1.8", + "Description": "Ensure that a Microsoft Entra diagnostic setting exists to send Microsoft Graph activity logs to an appropriate destination", + "Checks": [ + "monitor_diagnostic_settings_exists" + ], + "Attributes": [ + { + "Section": "6 Management and Governance Services", + "SubSection": "6.1 Logging and Monitoring", + "Profile": "Level 1", + "AssessmentStatus": "Manual", + "Description": "Ensure that a Microsoft Entra diagnostic setting is configured to send Microsoft Graph activity logs to a suitable destination, such as a Log Analytics workspace, storage account, or event hub. This enables centralized monitoring and analysis of all HTTP requests that the Microsoft Graph service receives and processes for a tenant.", + "RationaleStatement": "Microsoft Graph activity logs provide visibility into HTTP requests made to the Microsoft Graph service, helping detect unauthorized access, suspicious activity, and security threats. Configuring diagnostic settings in Microsoft Entra ensures these logs are collected and sent to an appropriate destination for monitoring, analysis, and retention.", + "ImpactStatement": "A Microsoft Entra ID P1 or P2 tenant license is required to access the Microsoft Graph activity logs. The amount of data logged and, thus, the cost incurred can vary significantly depending on the tenant size and the applications in your tenant that interact with the Microsoft Graph APIs. See the following pricing calculations for respective services: - Log Analytics: https://learn.microsoft.com/en-us/azure/azure-monitor/logs/cost-logs#pricing-model. - Azure Storage: https://azure.microsoft.com/en-gb/pricing/details/storage/blobs/. - Event Hubs: https://azure.microsoft.com/en-gb/pricing/details/event-hubs/", + "RemediationProcedure": "**Remediate from Azure Portal** 1. Go to `Microsoft Entra ID`. 1. Under `Monitoring`, click `Diagnostic settings`. 1. Click `+ Add diagnostic setting`. 1. Provide a `Diagnostic setting name`. 1. Under `Logs > Categories`, check the box next to `MicrosoftGraphActivityLogs`. 1. Configure an appropriate destination for the logs. 1. Click `Save`.", + "AuditProcedure": "**Audit from Azure Portal** 1. Go to `Microsoft Entra ID`. 1. Under `Monitoring`, click `Diagnostic settings`. 1. Next to each diagnostic setting, click `Edit setting`, and review the selected log categories and destination details. 1. Ensure that at least one diagnostic setting is configured to send `MicrosoftGraphActivityLogs` to an appropriate destination.", + "AdditionalInformation": "", + "References": "https://learn.microsoft.com/en-us/entra/identity/monitoring-health/howto-configure-diagnostic-settings:https://learn.microsoft.com/en-us/graph/microsoft-graph-activity-logs-overview:https://learn.microsoft.com/en-us/azure/azure-monitor/logs/cost-logs#pricing-model:https://azure.microsoft.com/en-gb/pricing/details/storage/blobs/:https://azure.microsoft.com/en-gb/pricing/details/event-hubs/", + "DefaultValue": "By default, Microsoft Entra diagnostic settings do not exist." + } + ] + }, + { + "Id": "6.1.1.9", + "Description": "Ensure that a Microsoft Entra diagnostic setting exists to send Microsoft Entra activity logs to an appropriate destination", + "Checks": [ + "monitor_diagnostic_settings_exists" + ], + "Attributes": [ + { + "Section": "6 Management and Governance Services", + "SubSection": "6.1 Logging and Monitoring", + "Profile": "Level 1", + "AssessmentStatus": "Manual", + "Description": "Ensure that a Microsoft Entra diagnostic setting is configured to send Microsoft Entra activity logs to a suitable destination, such as a Log Analytics workspace, storage account, or event hub. This enables centralized monitoring and analysis of Microsoft Entra activity logs.", + "RationaleStatement": "Microsoft Entra activity logs enables you to assess many aspects of your Microsoft Entra tenant. Configuring diagnostic settings in Microsoft Entra ensures these logs are collected and sent to an appropriate destination for monitoring, analysis, and retention.", + "ImpactStatement": "To export sign-in data, your organization needs an Azure AD P1 or P2 license. The amount of data logged and, thus, the cost incurred can vary significantly depending on the tenant size. See the following pricing calculations for respective services: - Log Analytics: https://learn.microsoft.com/en-us/azure/azure-monitor/logs/cost-logs#pricing-model. - Azure Storage: https://azure.microsoft.com/en-gb/pricing/details/storage/blobs/. - Event Hubs: https://azure.microsoft.com/en-gb/pricing/details/event-hubs/", + "RemediationProcedure": "**Remediate from Azure Portal** 1. Go to `Microsoft Entra ID`. 1. Under `Monitoring`, click `Diagnostic settings`. 1. Click `+ Add diagnostic setting`. 1. Provide a `Diagnostic setting name`. 1. Under `Logs > Categories`, check the box next to each of the following logs: - `AuditLogs` - `SignInLogs` - `NonInteractiveUserSignInLogs` - `ServicePrincipalSignInLogs` - `ManagedIdentitySignInLogs` - `ProvisioningLogs` - `ADFSSignInLogs` - `RiskyUsers` - `UserRiskEvents` - `NetworkAccessTrafficLogs` - `RiskyServicePrincipals` - `ServicePrincipalRiskEvents` - `EnrichedOffice365AuditLogs` - `MicrosoftGraphActivityLogs` - `RemoteNetworkHealthLogs` - `NetworkAccessAlerts` 1. Configure an appropriate destination for the logs. 1. Click `Save`.", + "AuditProcedure": "**Audit from Azure Portal** 1. Go to `Microsoft Entra ID`. 1. Under `Monitoring`, click `Diagnostic settings`. 1. Next to each diagnostic setting, click `Edit setting`, and review the selected log categories and destination details. 1. Ensure that at least one diagnostic setting is configured to send the following logs to an appropriate destination: - `AuditLogs` - `SignInLogs` - `NonInteractiveUserSignInLogs` - `ServicePrincipalSignInLogs` - `ManagedIdentitySignInLogs` - `ProvisioningLogs` - `ADFSSignInLogs` - `RiskyUsers` - `UserRiskEvents` - `NetworkAccessTrafficLogs` - `RiskyServicePrincipals` - `ServicePrincipalRiskEvents` - `EnrichedOffice365AuditLogs` - `MicrosoftGraphActivityLogs` - `RemoteNetworkHealthLogs` - `NetworkAccessAlerts`", + "AdditionalInformation": "", + "References": "https://learn.microsoft.com/en-us/entra/identity/monitoring-health/howto-configure-diagnostic-settings:https://learn.microsoft.com/en-us/entra/identity/monitoring-health/howto-access-activity-logs?tabs=microsoft-entra-activity-logs%2Carchive-activity-logs-to-a-storage-account", + "DefaultValue": "By default, Microsoft Entra diagnostic settings do not exist." + } + ] + }, + { + "Id": "6.1.1.10", + "Description": "Ensure that Intune logs are captured and sent to Log Analytics", + "Checks": [], + "Attributes": [ + { + "Section": "6 Management and Governance Services", + "SubSection": "6.1 Logging and Monitoring", + "Profile": "Level 1", + "AssessmentStatus": "Manual", + "Description": "Ensure that Intune logs are captured and fed into a central log analytics workspace.", + "RationaleStatement": "Intune includes built-in logs that provide information about your environments. Sending logs to a Log Analytics workspace enables centralized analysis, correlation, and alerting for faster threat detection and response.", + "ImpactStatement": "A Microsoft Intune plan is required to access Intune: https://www.microsoft.com/en-gb/security/business/microsoft-intune-pricing. The amount of data logged and, thus, the cost incurred can vary significantly depending on the tenant size. For information on Log Analytics workspace costs, visit: https://learn.microsoft.com/en-us/azure/azure-monitor/logs/cost-logs.", + "RemediationProcedure": "**Remediate from Azure Portal** 1. Go to `Intune`. 1. Click `Reports`. 1. Under `Azure monitor`, click `Diagnostic settings`. 1. Click `+ Add diagnostic setting`. 1. Provide a `Diagnostic setting name`. 1. Under `Logs > Categories`, check the box next to each of the following logs: - `AuditLogs` - `OperationalLogs` - `DeviceComplianceOrg` - `Devices` - `Windows365AuditLogs` 1. Under `Destination details`, check the box next to `Send to Log Analytics workspace`. 1. Select a `Subscription`. 1. Select a `Log Analytics workspace`. 1. Click `Save`.", + "AuditProcedure": "**Audit from Azure Portal** 1. Go to `Intune`. 1. Click `Reports`. 1. Under `Azure monitor`, click `Diagnostic settings`. 1. Next to each diagnostic setting, click `Edit setting`, and review the selected log categories and destination details. 1. Ensure that at least one diagnostic setting is configured to send the following logs to a Log Analytics workspace: - `AuditLogs` - `OperationalLogs` - `DeviceComplianceOrg` - `Devices` - `Windows365AuditLogs`", + "AdditionalInformation": "", + "References": "https://learn.microsoft.com/en-us/mem/intune/fundamentals/review-logs-using-azure-monitor:https://www.microsoft.com/en-gb/security/business/microsoft-intune-pricing:https://learn.microsoft.com/en-us/azure/azure-monitor/logs/cost-logs", + "DefaultValue": "By default, Intune diagnostic settings do not exist." + } + ] + }, + { + "Id": "6.1.2.1", + "Description": "Ensure that Activity Log Alert exists for Create Policy Assignment", + "Checks": [ + "monitor_alert_create_policy_assignment" + ], + "Attributes": [ + { + "Section": "6 Management and Governance Services", + "SubSection": "6.1 Logging and Monitoring", + "Profile": "Level 1", + "AssessmentStatus": "Automated", + "Description": "Create an activity log alert for the Create Policy Assignment event.", + "RationaleStatement": "Monitoring for create policy assignment events gives insight into changes done in Azure policy - assignments and can reduce the time it takes to detect unsolicited changes.", + "ImpactStatement": "Alert rules incur minimal costs.", + "RemediationProcedure": "**Remediate from Azure Portal** 1. Navigate to the `Monitor` blade. 1. Select `Alerts`. 1. Select `Create`. 1. Select `Alert rule`. 1. Choose a subscription. 1. Select `Apply`. 1. Select the `Condition` tab. 1. Click `See all signals`. 1. Select `Create policy assignment (Policy assignment)`. 1. Click `Apply`. 1. Select the `Actions` tab. 1. Click `Select action groups` to select an existing action group, or `Create action group` to create a new action group. 1. Follow the prompts to choose or create an action group. 1. Select the `Details` tab. 1. Select a `Resource group`, provide an `Alert rule name` and an optional `Alert rule description`. 1. Click `Review + create`. 1. Click `Create`. **Remediate from Azure CLI** ``` az monitor activity-log alert create --resource-group --condition category=Administrative and operationName=Microsoft.Authorization/policyAssignments/write and level= --scope /subscriptions/ --name --subscription --action-group ``` **Remediate from PowerShell** Create the `conditions` object. ``` $conditions = @() $conditions += New-AzActivityLogAlertAlertRuleAnyOfOrLeafConditionObject -Equal Administrative -Field category $conditions += New-AzActivityLogAlertAlertRuleAnyOfOrLeafConditionObject -Equal Microsoft.Authorization/policyAssignments/write -Field operationName $conditions += New-AzActivityLogAlertAlertRuleAnyOfOrLeafConditionObject -Equal Verbose -Field level ``` Get the `Action Group` information and store it in a variable, then create a new `Action` object. ``` $actionGroup = Get-AzActionGroup -ResourceGroupName -Name $actionObject = New-AzActivityLogAlertActionGroupObject -Id $actionGroup.Id ``` Create the `Scope` variable. ``` $scope = /subscriptions/ ``` Create the `Activity Log Alert Rule` for `Microsoft.Authorization/policyAssignments/write` ``` New-AzActivityLogAlert -Name -ResourceGroupName -Condition $conditions -Scope $scope -Location global -Action $actionObject -Subscription -Enabled $true ```", + "AuditProcedure": "**Audit from Azure Portal** 1. Navigate to the `Monitor` blade. 1. Click on `Alerts`. 1. In the Alerts window, click on `Alert rules`. 1. Ensure an alert rule exists where the Condition column contains `Operation name=Microsoft.Authorization/policyAssignments/write`. 1. Click on the Alert `Name` associated with the previous step. 1. Ensure the `Condition` panel displays the text `Whenever the Activity Log has an event with Category='Administrative', Operation name='Create policy assignment'` and does not filter on `Level`, `Status` or `Caller`. 1. Ensure the `Actions` panel displays an Action group is assigned to notify the appropriate personnel in your organization. **Audit from Azure CLI** ``` az monitor activity-log alert list --subscription --query [].{Name:name,Enabled:enabled,Condition:condition.allOf,Actions:actions} ``` Look for `Microsoft.Authorization/policyAssignments/write` in the output. If it's missing, generate a finding. **Audit from PowerShell** ``` Get-AzActivityLogAlert -SubscriptionId |where-object {$_.ConditionAllOf.Equal -match Microsoft.Authorization/policyAssignments/write}|select-object Location,Name,Enabled,ResourceGroupName,ConditionAllOf ``` If the output is empty, an `alert rule` for `Create Policy Assignments` is not configured. **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:** [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://azure.microsoft.com/en-us/updates/classic-alerting-monitoring-retirement:https://docs.microsoft.com/en-in/azure/azure-monitor/platform/alerts-activity-log:https://docs.microsoft.com/en-in/rest/api/monitor/activitylogalerts/createorupdate:https://docs.microsoft.com/en-in/rest/api/monitor/activitylogalerts/listbysubscriptionid:https://learn.microsoft.com/en-us/security/benchmark/azure/mcsb-logging-threat-detection#lt-3-enable-logging-for-security-investigation:https://docs.microsoft.com/en-in/rest/api/policy/policy-assignments:https://docs.microsoft.com/en-us/azure/azure-monitor/alerts/alerts-log", + "DefaultValue": "By default, no monitoring alerts are created." + } + ] + }, + { + "Id": "6.1.2.2", + "Description": "Ensure that Activity Log Alert exists for Delete Policy Assignment", + "Checks": [ + "monitor_alert_delete_policy_assignment" + ], + "Attributes": [ + { + "Section": "6 Management and Governance Services", + "SubSection": "6.1 Logging and Monitoring", + "Profile": "Level 1", + "AssessmentStatus": "Automated", + "Description": "Create an activity log alert for the Delete Policy Assignment event.", + "RationaleStatement": "Monitoring for delete policy assignment events gives insight into changes done in azure policy - assignments and can reduce the time it takes to detect unsolicited changes.", + "ImpactStatement": "Alert rules incur minimal costs.", + "RemediationProcedure": "**Remediate from Azure Portal** 1. Navigate to the `Monitor` blade. 1. Select `Alerts`. 1. Select `Create`. 1. Select `Alert rule`. 1. Choose a subscription. 1. Select `Apply`. 1. Select the `Condition` tab. 1. Click `See all signals`. 1. Select `Delete policy assignment (Policy assignment)`. 1. Click `Apply`. 1. Select the `Actions` tab. 1. Click `Select action groups` to select an existing action group, or `Create action group` to create a new action group. 1. Follow the prompts to choose or create an action group. 1. Select the `Details` tab. 1. Select a `Resource group`, provide an `Alert rule name` and an optional `Alert rule description`. 1. Click `Review + create`. 1. Click `Create`. **Remediate from Azure CLI** ``` az monitor activity-log alert create --resource-group --condition category=Administrative and operationName=Microsoft.Authorization/policyAssignments/delete and level= --scope /subscriptions/ --name --subscription --action-group ``` **Remediate from PowerShell** Create the conditions object ``` $conditions = @() $conditions += New-AzActivityLogAlertAlertRuleAnyOfOrLeafConditionObject -Equal Administrative -Field category $conditions += New-AzActivityLogAlertAlertRuleAnyOfOrLeafConditionObject -Equal Microsoft.Authorization/policyAssignments/delete -Field operationName $conditions += New-AzActivityLogAlertAlertRuleAnyOfOrLeafConditionObject -Equal Verbose -Field level ``` Retrieve the `Action Group` information and store in a variable, then create the `Action` object. ``` $actionGroup = Get-AzActionGroup -ResourceGroupName -Name $actionObject = New-AzActivityLogAlertActionGroupObject -Id $actionGroup.Id ``` Create the `Scope` variable. ``` $scope = /subscriptions/ ``` Create the `Activity Log Alert Rule` for `Microsoft.Authorization/policyAssignments/delete`. ``` New-AzActivityLogAlert -Name -ResourceGroupName -Condition $conditions -Scope $scope -Location global -Action $actionObject -Subscription -Enabled $true ```", + "AuditProcedure": "**Audit from Azure Portal** 1. Navigate to the `Monitor` blade. 1. Click on `Alerts`. 1. In the Alerts window, click on `Alert rules`. 1. Ensure an alert rule exists where the Condition column contains `Operation name=Microsoft.Authorization/policyAssignments/delete`. 1. Click on the Alert `Name` associated with the previous step. 1. Ensure the `Condition` panel displays the text `Whenever the Activity Log has an event with Category='Administrative', Operation name='Delete policy assignment'` and does not filter on `Level`, `Status` or `Caller`. 1. Ensure the `Actions` panel displays an Action group is assigned to notify the appropriate personnel in your organization. **Audit from Azure CLI** ``` az monitor activity-log alert list --subscription --query [].{Name:name,Enabled:enabled,Condition:condition.allOf,Actions:actions} ``` Look for `Microsoft.Authorization/policyAssignments/delete` in the output **Audit from PowerShell** ``` Get-AzActivityLogAlert -SubscriptionId |where-object {$_.ConditionAllOf.Equal -match Microsoft.Authorization/policyAssignments/delete}|select-object Location,Name,Enabled,ResourceGroupName,ConditionAllOf ``` **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:** [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": "This log alert also applies for Azure Blueprints.", + "References": "https://docs.microsoft.com/en-in/azure/azure-monitor/platform/alerts-activity-log:https://docs.microsoft.com/en-in/rest/api/monitor/activitylogalerts/createorupdate:https://docs.microsoft.com/en-in/rest/api/monitor/activitylogalerts/listbysubscriptionid:https://learn.microsoft.com/en-us/security/benchmark/azure/mcsb-logging-threat-detection#lt-3-enable-logging-for-security-investigation:https://azure.microsoft.com/en-us/services/blueprints/", + "DefaultValue": "By default, no monitoring alerts are created." + } + ] + }, + { + "Id": "6.1.2.3", + "Description": "Ensure that Activity Log Alert exists for Create or Update Network Security Group", + "Checks": [ + "monitor_alert_create_update_nsg" + ], + "Attributes": [ + { + "Section": "6 Management and Governance Services", + "SubSection": "6.1 Logging and Monitoring", + "Profile": "Level 1", + "AssessmentStatus": "Automated", + "Description": "Create an Activity Log Alert for the Create or Update Network Security Group event.", + "RationaleStatement": "Monitoring for Create or Update Network Security Group events gives insight into network access changes and may reduce the time it takes to detect suspicious activity.", + "ImpactStatement": "Alert rules incur minimal costs.", + "RemediationProcedure": "**Remediate from Azure Portal** 1. Navigate to the `Monitor` blade. 1. Select `Alerts`. 1. Select `Create`. 1. Select `Alert rule`. 1. Choose a subscription. 1. Select `Apply`. 1. Select the `Condition` tab. 1. Click `See all signals`. 1. Select `Create or Update Network Security Group (Network Security Group)`. 1. Click `Apply`. 1. Select the `Actions` tab. 1. Click `Select action groups` to select an existing action group, or `Create action group` to create a new action group. 1. Follow the prompts to choose or create an action group. 1. Select the `Details` tab. 1. Select a `Resource group`, provide an `Alert rule name` and an optional `Alert rule description`. 1. Click `Review + create`. 1. Click `Create`. **Remediate from Azure CLI** ``` az monitor activity-log alert create --resource-group --condition category=Administrative and operationName=Microsoft.Network/networkSecurityGroups/write and level=verbose --scope /subscriptions/ --name --subscription --action-group ``` **Remediate from PowerShell** Create the `Conditions` object. ``` $conditions = @() $conditions += New-AzActivityLogAlertAlertRuleAnyOfOrLeafConditionObject -Equal Administrative -Field category $conditions += New-AzActivityLogAlertAlertRuleAnyOfOrLeafConditionObject -Equal Microsoft.Network/networkSecurityGroups/write -Field operationName $conditions += New-AzActivityLogAlertAlertRuleAnyOfOrLeafConditionObject -Equal Verbose -Field level ``` Retrieve the `Action Group` information and store in a variable, then create the `Actions` object. ``` $actionGroup = Get-AzActionGroup -ResourceGroupName -Name $actionObject = New-AzActivityLogAlertActionGroupObject -Id $actionGroup.Id ``` Create the `Scope` object ``` $scope = /subscriptions/ ``` Create the `Activity Log Alert Rule` for `Microsoft.Network/networkSecurityGroups/write` ``` New-AzActivityLogAlert -Name -ResourceGroupName -Condition $conditions -Scope $scope -Location global -Action $actionObject -Subscription -Enabled $true ```", + "AuditProcedure": "**Audit from Azure Portal** 1. Navigate to the `Monitor` blade. 1. Click on `Alerts`. 1. In the Alerts window, click on `Alert rules`. 1. Ensure an alert rule exists where the Condition column contains `Operation name=Microsoft.Network/networkSecurityGroups/write`. 1. Click on the Alert `Name` associated with the previous step. 1. Ensure the `Condition` panel displays the text `Whenever the Activity Log has an event with Category='Administrative', Operation name='Create or Update Network Security Group'` and does not filter on `Level`, `Status` or `Caller`. 1. Ensure the `Actions` panel displays an Action group is assigned to notify the appropriate personnel in your organization. **Audit from Azure CLI** ``` az monitor activity-log alert list --subscription --query [].{Name:name,Enabled:enabled,Condition:condition.allOf,Actions:actions} ``` Look for `Microsoft.Network/networkSecurityGroups/write` in the output **Audit from PowerShell** ``` Get-AzActivityLogAlert -SubscriptionId |where-object {$_.ConditionAllOf.Equal -match Microsoft.Network/networkSecurityGroups/write}|select-object Location,Name,Enabled,ResourceGroupName,ConditionAllOf ``` **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:** [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'", + "AdditionalInformation": "", + "References": "https://azure.microsoft.com/en-us/updates/classic-alerting-monitoring-retirement:https://docs.microsoft.com/en-in/azure/azure-monitor/platform/alerts-activity-log:https://docs.microsoft.com/en-in/rest/api/monitor/activitylogalerts/createorupdate:https://docs.microsoft.com/en-in/rest/api/monitor/activitylogalerts/listbysubscriptionid:https://learn.microsoft.com/en-us/security/benchmark/azure/mcsb-logging-threat-detection#lt-3-enable-logging-for-security-investigation", + "DefaultValue": "By default, no monitoring alerts are created." + } + ] + }, + { + "Id": "6.1.2.4", + "Description": "Ensure that Activity Log Alert exists for Delete Network Security Group", + "Checks": [ + "monitor_alert_delete_nsg" + ], + "Attributes": [ + { + "Section": "6 Management and Governance Services", + "SubSection": "6.1 Logging and Monitoring", + "Profile": "Level 1", + "AssessmentStatus": "Automated", + "Description": "Create an activity log alert for the Delete Network Security Group event.", + "RationaleStatement": "Monitoring for Delete Network Security Group events gives insight into network access changes and may reduce the time it takes to detect suspicious activity.", + "ImpactStatement": "Alert rules incur minimal costs.", + "RemediationProcedure": "**Remediate from Azure Portal** 1. Navigate to the `Monitor` blade. 1. Select `Alerts`. 1. Select `Create`. 1. Select `Alert rule`. 1. Choose a subscription. 1. Select `Apply`. 1. Select the `Condition` tab. 1. Click `See all signals`. 1. Select `Delete Network Security Group (Network Security Group)`. 1. Click `Apply`. 1. Select the `Actions` tab. 1. Click `Select action groups` to select an existing action group, or `Create action group` to create a new action group. 1. Follow the prompts to choose or create an action group. 1. Select the `Details` tab. 1. Select a `Resource group`, provide an `Alert rule name` and an optional `Alert rule description`. 1. Click `Review + create`. 1. Click `Create`. **Remediate from Azure CLI** ``` az monitor activity-log alert create --resource-group --condition category=Administrative and operationName=Microsoft.Network/networkSecurityGroups/delete and level= --scope /subscriptions/ --name --subscription --action-group ``` **Remediate from PowerShell** Create the `Conditions` object. ``` $conditions = @() $conditions += New-AzActivityLogAlertAlertRuleAnyOfOrLeafConditionObject -Equal Administrative -Field category $conditions += New-AzActivityLogAlertAlertRuleAnyOfOrLeafConditionObject -Equal Microsoft.Network/networkSecurityGroups/delete -Field operationName $conditions += New-AzActivityLogAlertAlertRuleAnyOfOrLeafConditionObject -Equal Verbose -Field level ``` Retrieve the `Action Group` information and store in a variable, then create the `Actions` object. ``` $actionGroup = Get-AzActionGroup -ResourceGroupName -Name $actionObject = New-AzActivityLogAlertActionGroupObject -Id $actionGroup.Id ``` Create the `Scope` object ``` $scope = /subscriptions/ ``` Create the `Activity Log Alert Rule` for `Microsoft.Network/networkSecurityGroups/delete` ``` New-AzActivityLogAlert -Name -ResourceGroupName -Condition $conditions -Scope $scope -Location global -Action $actionObject -Subscription -Enabled $true ```", + "AuditProcedure": "**Audit from Azure Portal** 1. Navigate to the `Monitor` blade. 1. Click on `Alerts`. 1. In the Alerts window, click on `Alert rules`. 1. Ensure an alert rule exists where the Condition column contains `Operation name=Microsoft.Network/networkSecurityGroups/delete`. 1. Click on the Alert `Name` associated with the previous step. 1. Ensure the `Condition` panel displays the text `Whenever the Activity Log has an event with Category='Administrative', Operation name='Delete Network Security Group'` and does not filter on `Level`, `Status` or `Caller`. 1. Ensure the `Actions` panel displays an Action group is assigned to notify the appropriate personnel in your organization. **Audit from Azure CLI** ``` az monitor activity-log alert list --subscription --query [].{Name:name,Enabled:enabled,Condition:condition.allOf,Actions:actions} ``` Look for `Microsoft.Network/networkSecurityGroups/delete` in the output **Audit from PowerShell** ``` Get-AzActivityLogAlert -SubscriptionId |where-object {$_.ConditionAllOf.Equal -match Microsoft.Network/networkSecurityGroups/delete}|select-object Location,Name,Enabled,ResourceGroupName,ConditionAllOf ``` **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:** [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'", + "AdditionalInformation": "", + "References": "https://azure.microsoft.com/en-us/updates/classic-alerting-monitoring-retirement:https://docs.microsoft.com/en-in/azure/azure-monitor/platform/alerts-activity-log:https://docs.microsoft.com/en-in/rest/api/monitor/activitylogalerts/createorupdate:https://docs.microsoft.com/en-in/rest/api/monitor/activitylogalerts/listbysubscriptionid:https://learn.microsoft.com/en-us/security/benchmark/azure/mcsb-logging-threat-detection#lt-3-enable-logging-for-security-investigation", + "DefaultValue": "By default, no monitoring alerts are created." + } + ] + }, + { + "Id": "6.1.2.5", + "Description": "Ensure that Activity Log Alert exists for Create or Update Security Solution", + "Checks": [ + "monitor_alert_create_update_security_solution" + ], + "Attributes": [ + { + "Section": "6 Management and Governance Services", + "SubSection": "6.1 Logging and Monitoring", + "Profile": "Level 1", + "AssessmentStatus": "Automated", + "Description": "Create an activity log alert for the Create or Update Security Solution event.", + "RationaleStatement": "Monitoring for Create or Update Security Solution events gives insight into changes to the active security solutions and may reduce the time it takes to detect suspicious activity.", + "ImpactStatement": "Alert rules incur minimal costs.", + "RemediationProcedure": "**Remediate from Azure Portal** 1. Navigate to the `Monitor` blade. 1. Select `Alerts`. 1. Select `Create`. 1. Select `Alert rule`. 1. Choose a subscription. 1. Select `Apply`. 1. Select the `Condition` tab. 1. Click `See all signals`. 1. Select `Create or Update Security Solutions (Security Solutions)`. 1. Click `Apply`. 1. Select the `Actions` tab. 1. Click `Select action groups` to select an existing action group, or `Create action group` to create a new action group. 1. Follow the prompts to choose or create an action group. 1. Select the `Details` tab. 1. Select a `Resource group`, provide an `Alert rule name` and an optional `Alert rule description`. 1. Click `Review + create`. 1. Click `Create`. **Remediate from Azure CLI** ``` az monitor activity-log alert create --resource-group --condition category=Administrative and operationName=Microsoft.Security/securitySolutions/write and level= --scope /subscriptions/ --name --subscription --action-group ``` **Remediate from PowerShell** Create the `Conditions` object. ``` $conditions = @() $conditions += New-AzActivityLogAlertAlertRuleAnyOfOrLeafConditionObject -Equal Administrative -Field category $conditions += New-AzActivityLogAlertAlertRuleAnyOfOrLeafConditionObject -Equal Microsoft.Security/securitySolutions/write -Field operationName $conditions += New-AzActivityLogAlertAlertRuleAnyOfOrLeafConditionObject -Equal Verbose -Field level ``` Retrieve the `Action Group` information and store in a variable, then create the `Actions` object. ``` $actionGroup = Get-AzActionGroup -ResourceGroupName -Name $actionObject = New-AzActivityLogAlertActionGroupObject -Id $actionGroup.Id ``` Create the `Scope` object ``` $scope = /subscriptions/ ``` Create the `Activity Log Alert Rule` for `Microsoft.Security/securitySolutions/write` ``` New-AzActivityLogAlert -Name -ResourceGroupName -Condition $conditions -Scope $scope -Location global -Action $actionObject -Subscription -Enabled $true ```", + "AuditProcedure": "**Audit from Azure Portal** 1. Navigate to the `Monitor` blade. 1. Click on `Alerts`. 1. In the Alerts window, click on `Alert rules`. 1. Ensure an alert rule exists where the Condition column contains `Operation name=Microsoft.Security/securitySolutions/write`. 1. Click on the Alert `Name` associated with the previous step. 1. Ensure the `Condition` panel displays the text `Whenever the Activity Log has an event with Category='Administrative', Operation name='Create or Update Security Solutions'` and does not filter on `Level`, `Status` or `Caller`. 1. Ensure the `Actions` panel displays an Action group is assigned to notify the appropriate personnel in your organization. **Audit from Azure CLI** ``` az monitor activity-log alert list --subscription --query [].{Name:name,Enabled:enabled,Condition:condition.allOf,Actions:actions} ``` Look for `Microsoft.Security/securitySolutions/write` in the output **Audit from PowerShell** ``` Get-AzActivityLogAlert -SubscriptionId |where-object {$_.ConditionAllOf.Equal -match Microsoft.Security/securitySolutions/write}|select-object Location,Name,Enabled,ResourceGroupName,ConditionAllOf ``` **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:** [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'", + "AdditionalInformation": "", + "References": "https://azure.microsoft.com/en-us/updates/classic-alerting-monitoring-retirement:https://docs.microsoft.com/en-in/azure/azure-monitor/platform/alerts-activity-log:https://docs.microsoft.com/en-in/rest/api/monitor/activitylogalerts/createorupdate:https://docs.microsoft.com/en-in/rest/api/monitor/activitylogalerts/listbysubscriptionid:https://learn.microsoft.com/en-us/security/benchmark/azure/mcsb-logging-threat-detection#lt-3-enable-logging-for-security-investigation", + "DefaultValue": "By default, no monitoring alerts are created." + } + ] + }, + { + "Id": "6.1.2.6", + "Description": "Ensure that Activity Log Alert exists for Delete Security Solution", + "Checks": [ + "monitor_alert_delete_security_solution" + ], + "Attributes": [ + { + "Section": "6 Management and Governance Services", + "SubSection": "6.1 Logging and Monitoring", + "Profile": "Level 1", + "AssessmentStatus": "Automated", + "Description": "Create an activity log alert for the Delete Security Solution event.", + "RationaleStatement": "Monitoring for Delete Security Solution events gives insight into changes to the active security solutions and may reduce the time it takes to detect suspicious activity.", + "ImpactStatement": "Alert rules incur minimal costs.", + "RemediationProcedure": "**Remediate from Azure Portal** 1. Navigate to the `Monitor` blade. 1. Select `Alerts`. 1. Select `Create`. 1. Select `Alert rule`. 1. Choose a subscription. 1. Select `Apply`. 1. Select the `Condition` tab. 1. Click `See all signals`. 1. Select `Delete Security Solutions (Security Solutions)`. 1. Click `Apply`. 1. Select the `Actions` tab. 1. Click `Select action groups` to select an existing action group, or `Create action group` to create a new action group. 1. Follow the prompts to choose or create an action group. 1. Select the `Details` tab. 1. Select a `Resource group`, provide an `Alert rule name` and an optional `Alert rule description`. 1. Click `Review + create`. 1. Click `Create`. **Remediate from Azure CLI** ``` az monitor activity-log alert create --resource-group --condition category=Administrative and operationName=Microsoft.Security/securitySolutions/delete and level= --scope /subscriptions/ --name --subscription --action-group ``` **Remediate from PowerShell** Create the `Conditions` object. ``` $conditions = @() $conditions += New-AzActivityLogAlertAlertRuleAnyOfOrLeafConditionObject -Equal Administrative -Field category $conditions += New-AzActivityLogAlertAlertRuleAnyOfOrLeafConditionObject -Equal Microsoft.Security/securitySolutions/delete -Field operationName $conditions += New-AzActivityLogAlertAlertRuleAnyOfOrLeafConditionObject -Equal Verbose -Field level ``` Retrieve the `Action Group` information and store in a variable, then create the `Actions` object. ``` $actionGroup = Get-AzActionGroup -ResourceGroupName -Name $actionObject = New-AzActivityLogAlertActionGroupObject -Id $actionGroup.Id ``` Create the `Scope` object ``` $scope = /subscriptions/ ``` Create the `Activity Log Alert Rule` for `Microsoft.Security/securitySolutions/delete` ``` New-AzActivityLogAlert -Name -ResourceGroupName -Condition $conditions -Scope $scope -Location global -Action $actionObject -Subscription -Enabled $true ```", + "AuditProcedure": "**Audit from Azure Portal** 1. Navigate to the `Monitor` blade. 1. Click on `Alerts`. 1. In the Alerts window, click on `Alert rules`. 1. Ensure an alert rule exists where the Condition column contains `Operation name=Microsoft.Security/securitySolutions/delete`. 1. Click on the Alert `Name` associated with the previous step. 1. Ensure the `Condition` panel displays the text `Whenever the Activity Log has an event with Category='Administrative', Operation name='Delete Security Solutions'` and does not filter on `Level`, `Status` or `Caller`. 1. Ensure the `Actions` panel displays an Action group is assigned to notify the appropriate personnel in your organization. **Audit from Azure CLI** ``` az monitor activity-log alert list --subscription --query [].{Name:name,Enabled:enabled,Condition:condition.allOf,Actions:actions} ``` Look for `Microsoft.Security/securitySolutions/delete` in the output **Audit from PowerShell** ``` Get-AzActivityLogAlert -SubscriptionId |where-object {$_.ConditionAllOf.Equal -match Microsoft.Security/securitySolutions/delete}|select-object Location,Name,Enabled,ResourceGroupName,ConditionAllOf ``` **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:** [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'", + "AdditionalInformation": "", + "References": "https://azure.microsoft.com/en-us/updates/classic-alerting-monitoring-retirement:https://docs.microsoft.com/en-in/azure/azure-monitor/platform/alerts-activity-log:https://docs.microsoft.com/en-in/rest/api/monitor/activitylogalerts/createorupdate:https://docs.microsoft.com/en-in/rest/api/monitor/activitylogalerts/listbysubscriptionid:https://learn.microsoft.com/en-us/security/benchmark/azure/mcsb-logging-threat-detection#lt-3-enable-logging-for-security-investigation", + "DefaultValue": "By default, no monitoring alerts are created." + } + ] + }, + { + "Id": "6.1.2.7", + "Description": "Ensure that Activity Log Alert exists for Create or Update SQL Server Firewall Rule", + "Checks": [ + "monitor_alert_create_update_sqlserver_fr" + ], + "Attributes": [ + { + "Section": "6 Management and Governance Services", + "SubSection": "6.1 Logging and Monitoring", + "Profile": "Level 1", + "AssessmentStatus": "Automated", + "Description": "Create an activity log alert for the Create or Update SQL Server Firewall Rule event.", + "RationaleStatement": "Monitoring for Create or Update SQL Server Firewall Rule events gives insight into network access changes and may reduce the time it takes to detect suspicious activity.", + "ImpactStatement": "There will be a substantial increase in log size if there are a large number of administrative actions on a server.", + "RemediationProcedure": "**Remediate from Azure Portal** 1. Navigate to the `Monitor` blade. 1. Select `Alerts`. 1. Select `Create`. 1. Select `Alert rule`. 1. Choose a subscription. 1. Select `Apply`. 1. Select the `Condition` tab. 1. Click `See all signals`. 1. Select `Create/Update server firewall rule (Server Firewall Rule)`. 1. Click `Apply`. 1. Select the `Actions` tab. 1. Click `Select action groups` to select an existing action group, or `Create action group` to create a new action group. 1. Follow the prompts to choose or create an action group. 1. Select the `Details` tab. 1. Select a `Resource group`, provide an `Alert rule name` and an optional `Alert rule description`. 1. Click `Review + create`. 1. Click `Create`. **Remediate from Azure CLI** ``` az monitor activity-log alert create --resource-group --condition category=Administrative and operationName=Microsoft.Sql/servers/firewallRules/write and level= --scope /subscriptions/ --name --subscription --action-group ``` **Remediate from PowerShell** Create the `Conditions` object. ``` $conditions = @() $conditions += New-AzActivityLogAlertAlertRuleAnyOfOrLeafConditionObject -Equal Administrative -Field category $conditions += New-AzActivityLogAlertAlertRuleAnyOfOrLeafConditionObject -Equal Microsoft.Sql/servers/firewallRules/write -Field operationName $conditions += New-AzActivityLogAlertAlertRuleAnyOfOrLeafConditionObject -Equal Verbose -Field level ``` Retrieve the `Action Group` information and store in a variable, then create the `Actions` object. ``` $actionGroup = Get-AzActionGroup -ResourceGroupName -Name $actionObject = New-AzActivityLogAlertActionGroupObject -Id $actionGroup.Id ``` Create the `Scope` object ``` $scope = /subscriptions/ ``` Create the `Activity Log Alert Rule` for `Microsoft.Sql/servers/firewallRules/write` ``` New-AzActivityLogAlert -Name -ResourceGroupName -Condition $conditions -Scope $scope -Location global -Action $actionObject -Subscription -Enabled $true ```", + "AuditProcedure": "**Audit from Azure Portal** 1. Navigate to the `Monitor` blade. 1. Click on `Alerts`. 1. In the Alerts window, click on `Alert rules`. 1. Ensure an alert rule exists where the Condition column contains `Operation name=Microsoft.Sql/servers/firewallRules/write`. 1. Click on the Alert `Name` associated with the previous step. 1. Ensure the `Condition` panel displays the text `Whenever the Activity Log has an event with Category='Administrative', Operation name='Create/Update server firewall rule'` and does not filter on `Level`, `Status` or `Caller`. 1. Ensure the `Actions` panel displays an Action group is assigned to notify the appropriate personnel in your organization. **Audit from Azure CLI** ``` az monitor activity-log alert list --subscription --query [].{Name:name,Enabled:enabled,Condition:condition.allOf,Actions:actions} ``` Look for `Microsoft.Sql/servers/firewallRules/write` in the output **Audit from PowerShell** ``` Get-AzActivityLogAlert -SubscriptionId |where-object {$_.ConditionAllOf.Equal -match Microsoft.Sql/servers/firewallRules/write}|select-object Location,Name,Enabled,ResourceGroupName,ConditionAllOf ``` **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:** [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'", + "AdditionalInformation": "", + "References": "https://azure.microsoft.com/en-us/updates/classic-alerting-monitoring-retirement:https://docs.microsoft.com/en-in/azure/azure-monitor/platform/alerts-activity-log:https://docs.microsoft.com/en-in/rest/api/monitor/activitylogalerts/createorupdate:https://docs.microsoft.com/en-in/rest/api/monitor/activitylogalerts/listbysubscriptionid:https://learn.microsoft.com/en-us/security/benchmark/azure/mcsb-logging-threat-detection#lt-3-enable-logging-for-security-investigation", + "DefaultValue": "By default, no monitoring alerts are created." + } + ] + }, + { + "Id": "6.1.2.8", + "Description": "Ensure that Activity Log Alert exists for Delete SQL Server Firewall Rule", + "Checks": [ + "monitor_alert_delete_sqlserver_fr" + ], + "Attributes": [ + { + "Section": "6 Management and Governance Services", + "SubSection": "6.1 Logging and Monitoring", + "Profile": "Level 1", + "AssessmentStatus": "Automated", + "Description": "Create an activity log alert for the Delete SQL Server Firewall Rule.", + "RationaleStatement": "Monitoring for Delete SQL Server Firewall Rule events gives insight into SQL network access changes and may reduce the time it takes to detect suspicious activity.", + "ImpactStatement": "There will be a substantial increase in log size if there are a large number of administrative actions on a server.", + "RemediationProcedure": "**Remediate from Azure Portal** 1. Navigate to the `Monitor` blade. 1. Select `Alerts`. 1. Select `Create`. 1. Select `Alert rule`. 1. Choose a subscription. 1. Select `Apply`. 1. Select the `Condition` tab. 1. Click `See all signals`. 1. Select `Delete server firewall rule (Server Firewall Rule)`. 1. Click `Apply`. 1. Select the `Actions` tab. 1. Click `Select action groups` to select an existing action group, or `Create action group` to create a new action group. 1. Follow the prompts to choose or create an action group. 1. Select the `Details` tab. 1. Select a `Resource group`, provide an `Alert rule name` and an optional `Alert rule description`. 1. Click `Review + create`. 1. Click `Create`. **Remediate from Azure CLI** ``` az monitor activity-log alert create --resource-group --condition category=Administrative and operationName=Microsoft.Sql/servers/firewallRules/delete and level= --scope /subscriptions/ --name --subscription --action-group ``` **Remediate from PowerShell** Create the `Conditions` object. ``` $conditions = @() $conditions += New-AzActivityLogAlertAlertRuleAnyOfOrLeafConditionObject -Equal Administrative -Field category $conditions += New-AzActivityLogAlertAlertRuleAnyOfOrLeafConditionObject -Equal Microsoft.Sql/servers/firewallRules/delete -Field operationName $conditions += New-AzActivityLogAlertAlertRuleAnyOfOrLeafConditionObject -Equal Verbose -Field level ``` Retrieve the `Action Group` information and store in a variable, then create the `Actions` object. ``` $actionGroup = Get-AzActionGroup -ResourceGroupName -Name $actionObject = New-AzActivityLogAlertActionGroupObject -Id $actionGroup.Id ``` Create the `Scope` object ``` $scope = /subscriptions/ ``` Create the `Activity Log Alert Rule` for `Microsoft.Sql/servers/firewallRules/delete` ``` New-AzActivityLogAlert -Name -ResourceGroupName -Condition $conditions -Scope $scope -Location global -Action $actionObject -Subscription -Enabled $true ```", + "AuditProcedure": "**Audit from Azure Portal** 1. Navigate to the `Monitor` blade. 1. Click on `Alerts`. 1. In the Alerts window, click on `Alert rules`. 1. Ensure an alert rule exists where the Condition column contains `Operation name=Microsoft.Sql/servers/firewallRules/delete`. 1. Click on the Alert `Name` associated with the previous step. 1. Ensure the `Condition` panel displays the text `Whenever the Activity Log has an event with Category='Administrative', Operation name='Delete server firewall rule'` and does not filter on `Level`, `Status` or `Caller`. 1. Ensure the `Actions` panel displays an Action group is assigned to notify the appropriate personnel in your organization. **Audit from Azure CLI** ``` az monitor activity-log alert list --subscription --query [].{Name:name,Enabled:enabled,Condition:condition.allOf,Actions:actions} ``` Look for `Microsoft.Sql/servers/firewallRules/delete` in the output **Audit from PowerShell** ``` Get-AzActivityLogAlert -SubscriptionId |where-object {$_.ConditionAllOf.Equal -match Microsoft.Sql/servers/firewallRules/delete}|select-object Location,Name,Enabled,ResourceGroupName,ConditionAllOf ``` **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:** [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'", + "AdditionalInformation": "", + "References": "https://azure.microsoft.com/en-us/updates/classic-alerting-monitoring-retirement:https://docs.microsoft.com/en-in/azure/azure-monitor/platform/alerts-activity-log:https://docs.microsoft.com/en-in/rest/api/monitor/activitylogalerts/createorupdate:https://docs.microsoft.com/en-in/rest/api/monitor/activitylogalerts/listbysubscriptionid:https://learn.microsoft.com/en-us/security/benchmark/azure/mcsb-logging-threat-detection#lt-3-enable-logging-for-security-investigation", + "DefaultValue": "By default, no monitoring alerts are created." + } + ] + }, + { + "Id": "6.1.2.9", + "Description": "Ensure that Activity Log Alert exists for Create or Update Public IP Address rule", + "Checks": [ + "monitor_alert_create_update_public_ip_address_rule" + ], + "Attributes": [ + { + "Section": "6 Management and Governance Services", + "SubSection": "6.1 Logging and Monitoring", + "Profile": "Level 1", + "AssessmentStatus": "Automated", + "Description": "Create an activity log alert for the Create or Update Public IP Addresses rule.", + "RationaleStatement": "Monitoring for Create or Update Public IP Address events gives insight into network access changes and may reduce the time it takes to detect suspicious activity.", + "ImpactStatement": "There will be a substantial increase in log size if there are a large number of administrative actions on a server.", + "RemediationProcedure": "**Remediate from Azure Portal** 1. Navigate to the `Monitor` blade. 1. Select `Alerts`. 1. Select `Create`. 1. Select `Alert rule`. 1. Choose a subscription. 1. Select `Apply`. 1. Select the `Condition` tab. 1. Click `See all signals`. 1. Select `Create or Update Public Ip Address (Public Ip Address)`. 1. Click `Apply`. 1. Select the `Actions` tab. 1. Click `Select action groups` to select an existing action group, or `Create action group` to create a new action group. 1. Follow the prompts to choose or create an action group. 1. Select the `Details` tab. 1. Select a `Resource group`, provide an `Alert rule name` and an optional `Alert rule description`. 1. Click `Review + create`. 1. Click `Create`. **Remediate from Azure CLI** ``` az monitor activity-log alert create --resource-group --condition category=Administrative and operationName=Microsoft.Network/publicIPAddresses/write and level= --scope /subscriptions/ --name --subscription --action-group ``` **Remediate from PowerShell** Create the `Conditions` object. ``` $conditions = @() $conditions += New-AzActivityLogAlertAlertRuleAnyOfOrLeafConditionObject -Equal Administrative -Field category $conditions += New-AzActivityLogAlertAlertRuleAnyOfOrLeafConditionObject -Equal Microsoft.Network/publicIPAddresses/write -Field operationName $conditions += New-AzActivityLogAlertAlertRuleAnyOfOrLeafConditionObject -Equal Verbose -Field level ``` Retrieve the `Action Group` information and store in a variable, then create the `Actions` object. ``` $actionGroup = Get-AzActionGroup -ResourceGroupName -Name $actionObject = New-AzActivityLogAlertActionGroupObject -Id $actionGroup.Id ``` Create the `Scope` object ``` $scope = /subscriptions/ ``` Create the `Activity Log Alert Rule` for `Microsoft.Network/publicIPAddresses/write` ``` New-AzActivityLogAlert -Name -ResourceGroupName -Condition $conditions -Scope $scope -Location global -Action $actionObject -Subscription -Enabled $true ```", + "AuditProcedure": "**Audit from Azure Portal** 1. Navigate to the `Monitor` blade. 1. Click on `Alerts`. 1. In the Alerts window, click on `Alert rules`. 1. Ensure an alert rule exists where the Condition column contains `Operation name=Microsoft.Network/publicIPAddresses/write`. 1. Click on the Alert `Name` associated with the previous step. 1. Ensure the `Condition` panel displays the text `Whenever the Activity Log has an event with Category='Administrative', Operation name='Create or Update Public Ip Address'` and does not filter on `Level`, `Status` or `Caller`. 1. Ensure the `Actions` panel displays an Action group is assigned to notify the appropriate personnel in your organization. **Audit from Azure CLI** ``` az monitor activity-log alert list --subscription --query [].{Name:name,Enabled:enabled,Condition:condition.allOf,Actions:actions} ``` Look for `Microsoft.Network/publicIPAddresses/write` in the output **Audit from PowerShell** ``` Get-AzActivityLogAlert -SubscriptionId |where-object {$_.ConditionAllOf.Equal -match Microsoft.Network/publicIPAddresses/write}|select-object Location,Name,Enabled,ResourceGroupName,ConditionAllOf ``` **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:** [1513498c-3091-461a-b321-e9b433218d28](https://portal.azure.com/#view/Microsoft_Azure_Policy/PolicyDetailBlade/definitionId/%2Fproviders%2FMicrosoft.Authorization%2FpolicyDefinitions%2F1513498c-3091-461a-b321-e9b433218d28) **- Name:** 'Enable logging by category group for Public IP addresses (microsoft.network/publicipaddresses) to Log Analytics'", + "AdditionalInformation": "", + "References": "https://azure.microsoft.com/en-us/updates/classic-alerting-monitoring-retirement:https://docs.microsoft.com/en-in/azure/azure-monitor/platform/alerts-activity-log:https://docs.microsoft.com/en-in/rest/api/monitor/activitylogalerts/createorupdate:https://docs.microsoft.com/en-in/rest/api/monitor/activitylogalerts/listbysubscriptionid:https://learn.microsoft.com/en-us/security/benchmark/azure/mcsb-logging-threat-detection#lt-3-enable-logging-for-security-investigation", + "DefaultValue": "By default, no monitoring alerts are created." + } + ] + }, + { + "Id": "6.1.2.10", + "Description": "Ensure that Activity Log Alert exists for Delete Public IP Address rule", + "Checks": [ + "monitor_alert_delete_public_ip_address_rule" + ], + "Attributes": [ + { + "Section": "6 Management and Governance Services", + "SubSection": "6.1 Logging and Monitoring", + "Profile": "Level 1", + "AssessmentStatus": "Automated", + "Description": "Create an activity log alert for the Delete Public IP Address rule.", + "RationaleStatement": "Monitoring for Delete Public IP Address events gives insight into network access changes and may reduce the time it takes to detect suspicious activity.", + "ImpactStatement": "There will be a substantial increase in log size if there are a large number of administrative actions on a server.", + "RemediationProcedure": "**Remediate from Azure Portal** 1. Navigate to the `Monitor` blade. 1. Select `Alerts`. 1. Select `Create`. 1. Select `Alert rule`. 1. Choose a subscription. 1. Select `Apply`. 1. Select the `Condition` tab. 1. Click `See all signals`. 1. Select `Delete Public Ip Address (Public Ip Address)`. 1. Click `Apply`. 1. Select the `Actions` tab. 1. Click `Select action groups` to select an existing action group, or `Create action group` to create a new action group. 1. Follow the prompts to choose or create an action group. 1. Select the `Details` tab. 1. Select a `Resource group`, provide an `Alert rule name` and an optional `Alert rule description`. 1. Click `Review + create`. 1. Click `Create`. **Remediate from Azure CLI** ``` az monitor activity-log alert create --resource-group --condition category=Administrative and operationName=Microsoft.Network/publicIPAddresses/delete and level= --scope /subscriptions/ --name --subscription --action-group ``` **Remediate from PowerShell** Create the `Conditions` object. ``` $conditions = @() $conditions += New-AzActivityLogAlertAlertRuleAnyOfOrLeafConditionObject -Equal Administrative -Field category $conditions += New-AzActivityLogAlertAlertRuleAnyOfOrLeafConditionObject -Equal Microsoft.Network/publicIPAddresses/delete -Field operationName $conditions += New-AzActivityLogAlertAlertRuleAnyOfOrLeafConditionObject -Equal Verbose -Field level ``` Retrieve the `Action Group` information and store in a variable, then create the `Actions` object. ``` $actionGroup = Get-AzActionGroup -ResourceGroupName -Name $actionObject = New-AzActivityLogAlertActionGroupObject -Id $actionGroup.Id ``` Create the `Scope` object ``` $scope = /subscriptions/ ``` Create the `Activity Log Alert Rule` for `Microsoft.Network/publicIPAddresses/delete` ``` New-AzActivityLogAlert -Name -ResourceGroupName -Condition $conditions -Scope $scope -Location global -Action $actionObject -Subscription -Enabled $true ```", + "AuditProcedure": "**Audit from Azure Portal** 1. Navigate to the `Monitor` blade. 1. Click on `Alerts`. 1. In the Alerts window, click on `Alert rules`. 1. Ensure an alert rule exists where the Condition column contains `Operation name=Microsoft.Network/publicIPAddresses/delete`. 1. Click on the Alert `Name` associated with the previous step. 1. Ensure the `Condition` panel displays the text `Whenever the Activity Log has an event with Category='Administrative', Operation name='Delete Public Ip Address'` and does not filter on `Level`, `Status` or `Caller`. 1. Ensure the `Actions` panel displays an Action group is assigned to notify the appropriate personnel in your organization. **Audit from Azure CLI** ``` az monitor activity-log alert list --subscription --query [].{Name:name,Enabled:enabled,Condition:condition.allOf,Actions:actions} ``` Look for `Microsoft.Network/publicIPAddresses/delete` in the output **Audit from PowerShell** ``` Get-AzActivityLogAlert -SubscriptionId |where-object {$_.ConditionAllOf.Equal -match Microsoft.Network/publicIPAddresses/delete}|select-object Location,Name,Enabled,ResourceGroupName,ConditionAllOf ``` **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:** [1513498c-3091-461a-b321-e9b433218d28](https://portal.azure.com/#view/Microsoft_Azure_Policy/PolicyDetailBlade/definitionId/%2Fproviders%2FMicrosoft.Authorization%2FpolicyDefinitions%2F1513498c-3091-461a-b321-e9b433218d28) **- Name:** 'Enable logging by category group for Public IP addresses (microsoft.network/publicipaddresses) to Log Analytics'", + "AdditionalInformation": "", + "References": "https://azure.microsoft.com/en-us/updates/classic-alerting-monitoring-retirement:https://docs.microsoft.com/en-in/azure/azure-monitor/platform/alerts-activity-log:https://docs.microsoft.com/en-in/rest/api/monitor/activitylogalerts/createorupdate:https://docs.microsoft.com/en-in/rest/api/monitor/activitylogalerts/listbysubscriptionid:https://learn.microsoft.com/en-us/security/benchmark/azure/mcsb-logging-threat-detection#lt-3-enable-logging-for-security-investigation", + "DefaultValue": "By default, no monitoring alerts are created." + } + ] + }, + { + "Id": "6.1.2.11", + "Description": "Ensure that an Activity Log Alert exists for Service Health", + "Checks": [ + "monitor_alert_service_health_exists" + ], + "Attributes": [ + { + "Section": "6 Management and Governance Services", + "SubSection": "6.1 Logging and Monitoring", + "Profile": "Level 1", + "AssessmentStatus": "Automated", + "Description": "Create an activity log alert for Service Health.", + "RationaleStatement": "Monitoring for Service Health events provides insight into service issues, planned maintenance, security advisories, and other changes that may affect the Azure services and regions in use.", + "ImpactStatement": "There is no charge for creating activity log alert rules.", + "RemediationProcedure": "**Remediate from Azure Portal** 1. Go to `Monitor`. 1. Click `Alerts`. 1. Click `+ Create`. 1. Select `Alert rule` from the drop-down menu. 1. Choose a subscription. 1. Click `Apply`. 1. Select the `Condition` tab. 1. Click `See all signals`. 1. Select `Service health`. 1. Click `Apply`. 1. Open the drop-down menu next to `Event types`. 1. Check the box next to `Select all`. 1. Select the `Actions` tab. 1. Click `Select action groups` to select an existing action group, or `Create action group` to create a new action group. 1. Follow the prompts to choose or create an action group. 1. Select the `Details` tab. 1. Select a `Resource group`, provide an `Alert rule name` and an optional `Alert rule description`. 1. Click `Review + create`. 1. Click `Create`. 1. Repeat steps 1-19 for each subscription requiring remediation. **Remediate from Azure CLI** For each subscription requiring remediation, run the following command to create a `ServiceHealth` alert rule for a subscription: ``` az monitor activity-log alert create --subscription --resource-group --name --condition category=ServiceHealth and properties.incidentType=Incident --scope /subscriptions/ --action-group ``` **Remediate from PowerShell** Create the `Conditions` object: ``` $conditions = @() $conditions += New-AzActivityLogAlertAlertRuleAnyOfOrLeafConditionObject -Field category -Equal ServiceHealth $conditions += New-AzActivityLogAlertAlertRuleAnyOfOrLeafConditionObject -Field properties.incidentType -Equal Incident ``` Retrieve the `Action Group` information and store in a variable: ``` $actionGroup = Get-AzActionGroup -ResourceGroupName -Name ``` Create the `Actions` object: ``` $actionObject = New-AzActivityLogAlertActionGroupObject -Id $actionGroup.Id ``` Create the `Scope` object: ``` $scope = /subscriptions/ ``` Create the activity log alert rule: ``` New-AzActivityLogAlert -Name -ResourceGroupName -Condition $conditions -Scope $scope -Location global -Action $actionObject -Subscription -Enabled $true ``` Repeat for each subscription requiring remediation.", + "AuditProcedure": "**Audit from Azure Portal** 1. Go to `Monitor`. 1. Click `Alerts`. 1. Click `Alert rules`. 1. Ensure an alert rule exists for a subscription with `Condition` set to `Service names=All, Event types=All` and `Target resource type` set to `Subscription`. 1. If an alert rule is found for step 4, click the name of the alert rule. 1. Ensure the `Actions` panel displays an action group configured to notify appropriate personnel. 1. Repeat steps 1-6 for each subscription. **Audit from Azure CLI** Run the following command to list activity log alerts: ``` az monitor activity-log alert list --subscription ``` For each activity log alert, run the following command: ``` az monitor activity-log alert show --subscription --resource-group --activity-log-alert-name ``` Ensure an alert exists for `ServiceHealth` with `scopes` set to a subscription ID. Repeat for each subscription. **Audit from PowerShell** Run the following command to locate `ServiceHealth` alert rules for a subscription: ``` Get-AzActivityLogAlert -SubscriptionId | where-object {$_.ConditionAllOf.Equal -match ServiceHealth} | select-object Location,Name,Enabled,ResourceGroupName,ConditionAllOf ``` Ensure that at least one `ServiceHealth` alert rule is returned. Repeat for each subscription.", + "AdditionalInformation": "", + "References": "https://learn.microsoft.com/en-us/azure/service-health/overview:https://learn.microsoft.com/en-us/azure/service-health/alerts-activity-log-service-notifications-portal:https://azure.microsoft.com/en-gb/pricing/details/monitor/#faq:https://learn.microsoft.com/en-us/cli/azure/monitor/activity-log/alert:https://learn.microsoft.com/en-us/powershell/module/az.monitor/get-azactivitylogalert:https://learn.microsoft.com/en-us/powershell/module/az.monitor/new-azactivitylogalert", + "DefaultValue": "By default, no monitoring alerts are created." + } + ] + }, + { + "Id": "6.1.3.1", + "Description": "Ensure Application Insights are Configured", + "Checks": [ + "appinsights_ensure_is_configured" + ], + "Attributes": [ + { + "Section": "6 Management and Governance Services", + "SubSection": "6.1 Logging and Monitoring", + "Profile": "Level 1", + "AssessmentStatus": "Automated", + "Description": "Application Insights within Azure act as an Application Performance Monitoring solution providing valuable data into how well an application performs and additional information when performing incident response. The types of log data collected include application metrics, telemetry data, and application trace logging data providing organizations with detailed information about application activity and application transactions. Both data sets help organizations adopt a proactive and retroactive means to handle security and performance related metrics within their modern applications.", + "RationaleStatement": "Configuring Application Insights provides additional data not found elsewhere within Azure as part of a much larger logging and monitoring program within an organization's Information Security practice. The types and contents of these logs will act as both a potential cost saving measure (application performance) and a means to potentially confirm the source of a potential incident (trace logging). Metrics and Telemetry data provide organizations with a proactive approach to cost savings by monitoring an application's performance, while the trace logging data provides necessary details in a reactive incident response scenario by helping organizations identify the potential source of an incident within their application.", + "ImpactStatement": "Because Application Insights relies on a Log Analytics Workspace, an organization will incur additional expenses when using this service.", + "RemediationProcedure": "**Remediate from Azure Portal** 1. Navigate to `Application Insights`. 2. Under the `Basics` tab within the `PROJECT DETAILS` section, select the `Subscription`. 3. Select the `Resource group`. 4. Within the `INSTANCE DETAILS`, enter a `Name`. 5. Select a `Region`. 6. Next to `Resource Mode`, select `Workspace-based`. 7. Within the `WORKSPACE DETAILS`, select the `Subscription` for the log analytics workspace. 8. Select the appropriate `Log Analytics Workspace`. 9. Click `Next:Tags >`. 10. Enter the appropriate `Tags` as `Name`, `Value` pairs. 11. Click `Next:Review+Create`. 12. Click `Create`. **Remediate from Azure CLI** ``` az monitor app-insights component create --app --resource-group --location --kind web --retention-time --workspace --subscription ``` **Remediate from PowerShell** ``` New-AzApplicationInsights -Kind web -ResourceGroupName -Name -location -RetentionInDays -SubscriptionID -WorkspaceResourceId ```", + "AuditProcedure": "**Audit from Azure Portal** 1. Navigate to `Application Insights`. 2. Ensure an `Application Insights` service is configured and exists. **Audit from Azure CLI** ``` az monitor app-insights component show --query [].{ID:appId, Name:name, Tenant:tenantId, Location:location, Provisioning_State:provisioningState} ``` Ensure the above command produces output, otherwise `Application Insights` has not been configured. **Audit from PowerShell** ``` Get-AzApplicationInsights|select location,name,appid,provisioningState,tenantid ```", + "AdditionalInformation": "", + "References": "https://learn.microsoft.com/en-us/azure/azure-monitor/app/app-insights-overview", + "DefaultValue": "Application Insights are not enabled by default." + } + ] + }, + { + "Id": "6.1.4", + "Description": "Ensure that Azure Monitor Resource Logging is Enabled for All Services that Support it", + "Checks": [ + "monitor_diagnostic_settings_exists" + ], + "Attributes": [ + { + "Section": "6 Management and Governance Services", + "SubSection": "6.1 Logging and Monitoring", + "Profile": "Level 1", + "AssessmentStatus": "Manual", + "Description": "Resource Logs capture activity to the data access plane while the Activity log is a subscription-level log for the control plane. Resource-level diagnostic logs provide insight into operations that were performed within that resource itself; for example, reading or updating a secret from a Key Vault. Currently, 95 Azure resources support Azure Monitoring (See the more information section for a complete list), including Network Security Groups, Load Balancers, Key Vault, AD, Logic Apps, and CosmosDB. The content of these logs varies by resource type. A number of back-end services were not configured to log and store Resource Logs for certain activities or for a sufficient length. It is crucial that monitoring is correctly configured to log all relevant activities and retain those logs for a sufficient length of time. Given that the mean time to detection in an enterprise is 240 days, a minimum retention period of two years is recommended.", + "RationaleStatement": "A lack of monitoring reduces the visibility into the data plane, and therefore an organization's ability to detect reconnaissance, authorization attempts or other malicious activity. Unlike Activity Logs, Resource Logs are not enabled by default. Specifically, without monitoring it would be impossible to tell which entities had accessed a data store that was breached. In addition, alerts for failed attempts to access APIs for Web Services or Databases are only possible when logging is enabled.", + "ImpactStatement": "Costs for monitoring varies with Log Volume. Not every resource needs to have logging enabled. It is important to determine the security classification of the data being processed by the given resource and adjust the logging based on which events need to be tracked. This is typically determined by governance and compliance requirements.", + "RemediationProcedure": "Azure Subscriptions should log every access and operation for all resources. Logs should be sent to Storage and a Log Analytics Workspace or equivalent third-party system. Logs should be kept in readily-accessible storage for a minimum of one year, and then moved to inexpensive cold storage for a duration of time as necessary. If retention policies are set but storing logs in a Storage Account is disabled (for example, if only Event Hubs or Log Analytics options are selected), the retention policies have no effect. Enable all monitoring at first, and then be more aggressive moving data to cold storage if the volume of data becomes a cost concern. **Remediate from Azure Portal** The specific steps for configuring resources within the Azure console vary depending on resource, but typically the steps are: 1. Go to the resource 2. Click on Diagnostic settings 3. In the blade that appears, click Add diagnostic setting 4. Configure the diagnostic settings 5. Click on Save **Remediate from Azure CLI** For each `resource`, run the following making sure to use a `resource` appropriate JSON encoded `category` for the `--logs` option. ``` az monitor diagnostic-settings create --name --resource --logs [{category:,enabled:true,rentention-policy:{enabled:true,days:180}}] --metrics [{category:AllMetrics,enabled:true,retention-policy:{enabled:true,days:180}}] <[--event-hub --event-hub-rule | --storage-account |--workspace | --marketplace-partner-id ]> ``` **Remediate from PowerShell** Create the `log` settings object ``` $logSettings = @() $logSettings += New-AzDiagnosticSettingLogSettingsObject -Enabled $true -RetentionPolicyDay 180 -RetentionPolicyEnabled $true -Category $logSettings += New-AzDiagnosticSettingLogSettingsObject -Enabled $true -RetentionPolicyDay 180 -RetentionPolicyEnabled $true -Category ``` Create the `metric` settings object ``` $metricSettings = @() $metricSettings += New-AzDiagnosticSettingMetricSettingsObject -Enabled $true -RetentionPolicyDay 180 -RetentionPolicyEnabled $true -Category AllMetrics ``` Create the diagnostic setting for a specific resource ``` New-AzDiagnosticSetting -Name -ResourceId -Log $logSettings -Metric $metricSettings ```", + "AuditProcedure": "**Audit from Azure Portal** The specific steps for configuring resources within the Azure console vary depending on resource, but typically the steps are: 1. Go to the resource 2. Click on Diagnostic settings 3. In the blade that appears, click Add diagnostic setting 4. Configure the diagnostic settings 5. Click on Save **Audit from Azure CLI** List all `resources` for a `subscription` ``` az resource list --subscription ``` For each `resource` run the following ``` az monitor diagnostic-settings list --resource ``` An empty result means a `diagnostic settings` is not configured for that resource. An error message means a `diagnostic settings` is not supported for that resource. **Audit from PowerShell** Get a list of `resources` in a `subscription` context and store in a variable ``` $resources = Get-AzResource ``` Loop through each `resource` to determine if a diagnostic setting is configured or not. ``` foreach ($resource in $resources) {$diagnosticSetting = Get-AzDiagnosticSetting -ResourceId $resource.id -ErrorAction SilentlyContinue; if ([string]::IsNullOrEmpty($diagnosticSetting)) {$message = Diagnostic Settings not configured for resource: + $resource.Name;Write-Output $message}else{$diagnosticSetting}} ``` A result of `Diagnostic Settings not configured for resource: ` means a `diagnostic settings` is not configured for that resource. Otherwise, the output of the above command will show configured `Diagnostic Settings` for a resource. **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:** [cf820ca0-f99e-4f3e-84fb-66e913812d21](https://portal.azure.com/#view/Microsoft_Azure_Policy/PolicyDetailBlade/definitionId/%2Fproviders%2FMicrosoft.Authorization%2FpolicyDefinitions%2Fcf820ca0-f99e-4f3e-84fb-66e913812d21) **- Name:** 'Resource logs in Key Vault should be enabled' - **Policy ID:** [91a78b24-f231-4a8a-8da9-02c35b2b6510](https://portal.azure.com/#view/Microsoft_Azure_Policy/PolicyDetailBlade/definitionId/%2Fproviders%2FMicrosoft.Authorization%2FpolicyDefinitions%2F91a78b24-f231-4a8a-8da9-02c35b2b6510) **- Name:** 'App Service apps should have resource logs enabled' - **Policy ID:** [428256e6-1fac-4f48-a757-df34c2b3336d](https://portal.azure.com/#view/Microsoft_Azure_Policy/PolicyDetailBlade/definitionId/%2Fproviders%2FMicrosoft.Authorization%2FpolicyDefinitions%2F428256e6-1fac-4f48-a757-df34c2b3336d) **- Name:** 'Resource logs in Batch accounts should be enabled' - **Policy ID:** [057ef27e-665e-4328-8ea3-04b3122bd9fb](https://portal.azure.com/#view/Microsoft_Azure_Policy/PolicyDetailBlade/definitionId/%2Fproviders%2FMicrosoft.Authorization%2FpolicyDefinitions%2F057ef27e-665e-4328-8ea3-04b3122bd9fb) **- Name:** 'Resource logs in Azure Data Lake Store should be enabled' - **Policy ID:** [c95c74d9-38fe-4f0d-af86-0c7d626a315c](https://portal.azure.com/#view/Microsoft_Azure_Policy/PolicyDetailBlade/definitionId/%2Fproviders%2FMicrosoft.Authorization%2FpolicyDefinitions%2Fc95c74d9-38fe-4f0d-af86-0c7d626a315c) **- Name:** 'Resource logs in Data Lake Analytics should be enabled' - **Policy ID:** [83a214f7-d01a-484b-91a9-ed54470c9a6a](https://portal.azure.com/#view/Microsoft_Azure_Policy/PolicyDetailBlade/definitionId/%2Fproviders%2FMicrosoft.Authorization%2FpolicyDefinitions%2F83a214f7-d01a-484b-91a9-ed54470c9a6a) **- Name:** 'Resource logs in Event Hub should be enabled' - **Policy ID:** [383856f8-de7f-44a2-81fc-e5135b5c2aa4](https://portal.azure.com/#view/Microsoft_Azure_Policy/PolicyDetailBlade/definitionId/%2Fproviders%2FMicrosoft.Authorization%2FpolicyDefinitions%2F383856f8-de7f-44a2-81fc-e5135b5c2aa4) **- Name:** 'Resource logs in IoT Hub should be enabled' - **Policy ID:** [34f95f76-5386-4de7-b824-0d8478470c9d](https://portal.azure.com/#view/Microsoft_Azure_Policy/PolicyDetailBlade/definitionId/%2Fproviders%2FMicrosoft.Authorization%2FpolicyDefinitions%2F34f95f76-5386-4de7-b824-0d8478470c9d) **- Name:** 'Resource logs in Logic Apps should be enabled' - **Policy ID:** [b4330a05-a843-4bc8-bf9a-cacce50c67f4](https://portal.azure.com/#view/Microsoft_Azure_Policy/PolicyDetailBlade/definitionId/%2Fproviders%2FMicrosoft.Authorization%2FpolicyDefinitions%2Fb4330a05-a843-4bc8-bf9a-cacce50c67f4) **- Name:** 'Resource logs in Search services should be enabled' - **Policy ID:** [f8d36e2f-389b-4ee4-898d-21aeb69a0f45](https://portal.azure.com/#view/Microsoft_Azure_Policy/PolicyDetailBlade/definitionId/%2Fproviders%2FMicrosoft.Authorization%2FpolicyDefinitions%2Ff8d36e2f-389b-4ee4-898d-21aeb69a0f45) **- Name:** 'Resource logs in Service Bus should be enabled' - **Policy ID:** [f9be5368-9bf5-4b84-9e0a-7850da98bb46](https://portal.azure.com/#view/Microsoft_Azure_Policy/PolicyDetailBlade/definitionId/%2Fproviders%2FMicrosoft.Authorization%2FpolicyDefinitions%2Ff9be5368-9bf5-4b84-9e0a-7850da98bb46) **- Name:** 'Resource logs in Azure Stream Analytics should be enabled'", + "AdditionalInformation": "For an up-to-date list of Azure resources which support Azure Monitor, refer to the Supported Log Categories reference.", + "References": "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/security/benchmark/azure/mcsb-logging-threat-detection#lt-5-centralize-security-log-management-and-analysis:https://docs.microsoft.com/en-us/azure/azure-monitor/essentials/monitor-azure-resource:Supported Log Categories: https://docs.microsoft.com/en-us/azure/azure-monitor/essentials/resource-logs-categories:Logs and Audit - Fundamentals: https://docs.microsoft.com/en-us/azure/security/fundamentals/log-audit:Collecting Logs: https://docs.microsoft.com/en-us/azure/azure-monitor/platform/collect-activity-logs:Key Vault Logging: https://docs.microsoft.com/en-us/azure/key-vault/key-vault-logging:Monitor Diagnostic Settings: https://docs.microsoft.com/en-us/cli/azure/monitor/diagnostic-settings?view=azure-cli-latest:Overview of Diagnostic Logs: https://docs.microsoft.com/en-us/azure/azure-monitor/platform/diagnostic-logs-overview:Supported Services for Diagnostic Logs: https://docs.microsoft.com/en-us/azure/azure-monitor/platform/diagnostic-logs-schema:Diagnostic Logs for CDNs: https://docs.microsoft.com/en-us/azure/cdn/cdn-azure-diagnostic-logs", + "DefaultValue": "By default, Azure Monitor Resource Logs are 'Disabled' for all resources." + } + ] + }, + { + "Id": "6.1.5", + "Description": "Ensure that SKU Basic/Consumption is not used on artifacts that need to be monitored (Particularly for Production Workloads)", + "Checks": [], + "Attributes": [ + { + "Section": "6 Management and Governance Services", + "SubSection": "6.1 Logging and Monitoring", + "Profile": "Level 1", + "AssessmentStatus": "Manual", + "Description": "The use of Basic or Free SKUs in Azure whilst cost effective have significant limitations in terms of what can be monitored and what support can be realized from Microsoft. Typically, these SKUs do not have a service SLA and Microsoft may refuse to provide support for them. Consequently Basic/Free SKUs should never be used for production workloads.", + "RationaleStatement": "Typically, production workloads need to be monitored and should have an SLA with Microsoft, using Basic SKUs for any deployed product will mean that that these capabilities do not exist. The following resource types should use standard SKUs as a minimum. - Public IP Addresses - Network Load Balancers - REDIS Cache - SQL PaaS Databases - VPN Gateways", + "ImpactStatement": "The impact of enforcing Standard SKU's is twofold 1) There will be a cost increase 2) The monitoring and service level agreements will be available and will support the production service. All resources should be either tagged or in separate Management Groups/Subscriptions", + "RemediationProcedure": "Each resource has its own process for upgrading from basic to standard SKUs that should be followed if required. - Public IP Address: https://learn.microsoft.com/en-us/azure/virtual-network/ip-services/public-ip-upgrade. - Basic Load Balancer: https://learn.microsoft.com/en-us/azure/load-balancer/load-balancer-basic-upgrade-guidance. - Azure Cache for Redis: https://learn.microsoft.com/en-us/azure/azure-cache-for-redis/cache-how-to-scale. - Azure SQL Database: https://learn.microsoft.com/en-us/azure/azure-sql/database/scale-resources. - VPN Gateway: https://learn.microsoft.com/en-us/azure/vpn-gateway/gateway-sku-resize.", + "AuditProcedure": "This needs to be audited by Azure Policy (one for each resource type) and denied for each artifact that is production. **Audit from Azure Portal** 1. Open `Azure Resource Graph Explorer` 1. Click `New query` 1. Paste the following into the query window: ``` Resources | where sku contains 'Basic' or sku contains 'consumption' | order by type ``` 4. Click `Run query` then evaluate the results in the results window. 5. Ensure that no production artifacts are returned. **Audit from Azure CLI** ``` az graph query -q Resources | where sku contains 'Basic' or sku contains 'consumption' | order by type ``` Alternatively, to filter on a specific resource group: ``` az graph query -q Resources | where resourceGroup == '' | where sku contains 'Basic' or sku contains 'consumption' | order by type ``` Ensure that no production artifacts are returned. **Audit from PowerShell** ``` Get-AzResource | ?{ $_.Sku -EQ Basic} ``` Ensure that no production artifacts are returned.", + "AdditionalInformation": "", + "References": "https://azure.microsoft.com/en-us/support/plans:https://azure.microsoft.com/en-us/support/plans/response/:https://learn.microsoft.com/en-us/azure/virtual-network/ip-services/public-ip-upgrade:https://learn.microsoft.com/en-us/azure/load-balancer/load-balancer-basic-upgrade-guidance:https://learn.microsoft.com/en-us/azure/azure-cache-for-redis/cache-how-to-scale:https://learn.microsoft.com/en-us/azure/azure-sql/database/scale-resources:https://learn.microsoft.com/en-us/azure/vpn-gateway/gateway-sku-resize", + "DefaultValue": "Policy should enforce standard SKUs for the following artifacts: - Public IP Addresses - Network Load Balancers - REDIS Cache - SQL PaaS Databases - VPN Gateways" + } + ] + }, + { + "Id": "6.2", + "Description": "Ensure that Resource Locks are set for Mission-Critical Azure Resources", + "Checks": [ + "iam_custom_role_has_permissions_to_administer_resource_locks" + ], + "Attributes": [ + { + "Section": "6 Management and Governance Services", + "Profile": "Level 1", + "AssessmentStatus": "Manual", + "Description": "Resource Manager Locks provide a way for administrators to lock down Azure resources to prevent deletion of, or modifications to, a resource. These locks sit outside of the Role Based Access Controls (RBAC) hierarchy and, when applied, will place restrictions on the resource for all users. These locks are very useful when there is an important resource in a subscription that users should not be able to delete or change. Locks can help prevent accidental and malicious changes or deletion.", + "RationaleStatement": "As an administrator, it may be necessary to lock a subscription, resource group, or resource to prevent other users in the organization from accidentally deleting or modifying critical resources. The lock level can be set to to `CanNotDelete` or `ReadOnly` to achieve this purpose. - `CanNotDelete` means authorized users can still read and modify a resource, but they cannot delete the resource. - `ReadOnly` means authorized users can read a resource, but they cannot delete or update the resource. Applying this lock is similar to restricting all authorized users to the permissions granted by the Reader role.", + "ImpactStatement": "There can be unintended outcomes of locking a resource. Applying a lock to a parent service will cause it to be inherited by all resources within. Conversely, applying a lock to a resource may not apply to connected storage, leaving it unlocked. Please see the documentation for further information.", + "RemediationProcedure": "**Remediate from Azure Portal** 1. Navigate to the specific Azure Resource or Resource Group. 2. For each mission critical resource, click on `Locks`. 3. Click `Add`. 4. Give the lock a name and a description, then select the type, `Read-only` or `Delete` as appropriate. 5. Click OK. **Remediate from Azure CLI** To lock a resource, provide the name of the resource, its resource type, and its resource group name. ``` az lock create --name --lock-type --resource-group --resource-name --resource-type ``` **Remediate from PowerShell** ``` Get-AzResourceLock -ResourceName -ResourceType -ResourceGroupName -Locktype ```", + "AuditProcedure": "**Audit from Azure Portal** 1. Navigate to the specific Azure Resource or Resource Group. 2. Click on `Locks`. 3. Ensure the lock is defined with name and description, with type `Read-only` or `Delete` as appropriate. **Audit from Azure CLI** Review the list of all locks set currently: ``` az lock list --resource-group --resource-name --namespace --resource-type --parent ``` **Audit from PowerShell** Run the following command to list all resources. ``` Get-AzResource ``` For each resource, run the following command to check for Resource Locks. ``` Get-AzResourceLock -ResourceName -ResourceType -ResourceGroupName ``` Review the output of the `Properties` setting. Compliant settings will have the `CanNotDelete` or `ReadOnly` value.", + "AdditionalInformation": "", + "References": "https://docs.microsoft.com/en-us/azure/azure-resource-manager/resource-group-lock-resources:https://docs.microsoft.com/en-us/azure/azure-resource-manager/resource-manager-subscription-governance#azure-resource-locks:https://docs.microsoft.com/en-us/azure/governance/blueprints/concepts/resource-locking:https://learn.microsoft.com/en-us/security/benchmark/azure/mcsb-asset-management#am-4-limit-access-to-asset-management", + "DefaultValue": "By default, no locks are set." + } + ] + }, + { + "Id": "7.1", + "Description": "Ensure that RDP access from the Internet is evaluated and restricted", + "Checks": [ + "network_rdp_internet_access_restricted" + ], + "Attributes": [ + { + "Section": "7 Networking Services", + "Profile": "Level 1", + "AssessmentStatus": "Automated", + "Description": "Network security groups should be periodically evaluated for port misconfigurations. Where RDP is not explicitly required and narrowly configured for resources attached to a network security group, Internet-level access to Azure resources should be restricted or eliminated.", + "RationaleStatement": "The potential security problem with using RDP over the Internet is that attackers can use various brute force techniques to gain access to Azure Virtual Machines. Once the attackers gain access, they can use a virtual machine as a launch point for compromising other machines on an Azure Virtual Network or even attack networked devices outside of Azure.", + "ImpactStatement": "Restricting RDP access may require alternative methods for remote administration such as VPN or Azure Bastion.", + "RemediationProcedure": "**Remediate from Azure Portal** 1. Go to `Network security groups`. 1. Under `Settings`, click `Inbound security rules`. 1. Check the box next to any inbound security rule matching: - Port: `3389` or range including 3389 - Protocol: `TCP` or `Any` - Source: `0.0.0.0/0`, `Internet`, or `Any` - Action: `Allow` 1. Click `Delete`. 1. Click `Yes`. **Remediate from Azure CLI** For each network security group rule requiring remediation, run the following command to delete a rule: ``` az network nsg rule delete --resource-group --nsg-name --name ```", + "AuditProcedure": "**Audit from Azure Portal** 1. Go to `Network security groups`. 1. Under `Settings`, click `Inbound security rules`. 1. Ensure that no inbound security rule exists that matches the following: - Port: `3389` or range including 3389 - Protocol: `TCP` or `Any` - Source: `0.0.0.0/0`, `Internet`, or `Any` - Action: `Allow` 1. Repeat steps 1-3 for each network security group. To audit from Azure Resource Graph: 1. Go to `Resource Graph Explorer`. 1. Click `New query`. 1. Paste the following into the query window: ``` resources | where type =~ microsoft.network/networksecuritygroups | project id, name, securityRule = properties.securityRules | mv-expand securityRule | extend access = securityRule.properties.access, direction = securityRule.properties.direction, protocol = securityRule.properties.protocol, destinationPort = case(isempty(securityRule.properties.destinationPortRange), securityRule.properties.destinationPortRanges, securityRule.properties.destinationPortRange), sourceAddress = case(isempty(securityRule.properties.sourceAddressPrefix), securityRule.properties.sourceAddressPrefixes, securityRule.properties.sourceAddressPrefix) | where access =~ Allow and direction =~ Inbound and protocol in~ (tcp, ) | mv-expand destinationPort | mv-expand sourceAddress | extend destinationPortMin = toint(split(destinationPort, -)[0]), destinationPortMax = toint(split(destinationPort, -)[-1]) | where (destinationPortMin <= 3389 and destinationPortMax >= 3389) or destinationPort == | where sourceAddress in~ (*, 0.0.0.0, internet, any) or sourceAddress endswith /0 ``` 1. Click `Run query`. 1. Ensure that no results are returned. **Audit from Azure CLI** List network security groups with non-default security rules: ``` az network nsg list --query [*].[name,securityRules] ``` Ensure that no network security group has an inbound security rule that matches the following: ``` access : Allow destinationPortRange : 3389, *, or direction : Inbound protocol : TCP or * sourceAddressPrefix : 0.0.0.0/0, Internet, or * ``` **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:** [22730e10-96f6-4aac-ad84-9383d35b5917](https://portal.azure.com/#view/Microsoft_Azure_Policy/PolicyDetailBlade/definitionId/%2Fproviders%2FMicrosoft.Authorization%2FpolicyDefinitions%2F22730e10-96f6-4aac-ad84-9383d35b5917) **- Name:** 'Management ports should be closed on your virtual machines'", + "AdditionalInformation": "", + "References": "https://docs.microsoft.com/en-us/azure/security/azure-security-network-security-best-practices#disable-rdpssh-access-to-azure-virtual-machines:https://learn.microsoft.com/en-us/security/benchmark/azure/mcsb-network-security#ns-1-establish-network-segmentation-boundaries:Express Route: https://docs.microsoft.com/en-us/azure/expressroute/:Site-to-Site VPN: https://docs.microsoft.com/en-us/azure/vpn-gateway/vpn-gateway-howto-site-to-site-resource-manager-portal:Point-to-Site VPN: https://docs.microsoft.com/en-us/azure/vpn-gateway/vpn-gateway-howto-point-to-site-resource-manager-portal", + "DefaultValue": "By default, RDP access from internet is not `enabled`." + } + ] + }, + { + "Id": "7.2", + "Description": "Ensure that SSH access from the Internet is evaluated and restricted", + "Checks": [ + "network_ssh_internet_access_restricted" + ], + "Attributes": [ + { + "Section": "7 Networking Services", + "Profile": "Level 1", + "AssessmentStatus": "Automated", + "Description": "Network security groups should be periodically evaluated for port misconfigurations. Where certain ports and protocols may be exposed to the Internet, they should be evaluated for necessity and restricted wherever they are not explicitly required.", + "RationaleStatement": "The potential security problem with using SSH over the Internet is that attackers can use various brute force techniques to gain access to Azure Virtual Machines. Once the attackers gain access, they can use a virtual machine as a launch point for compromising other machines on the Azure Virtual Network or even attack networked devices outside of Azure.", + "ImpactStatement": "Restricting SSH access may require alternative methods for remote administration such as VPN or Azure Bastion.", + "RemediationProcedure": "Where SSH is not explicitly required and narrowly configured for resources attached to the Network Security Group, Internet-level access to your Azure resources should be restricted or eliminated. For internal access to relevant resources, configure an encrypted network tunnel such as: [ExpressRoute](https://docs.microsoft.com/en-us/azure/expressroute/) [Site-to-site VPN](https://docs.microsoft.com/en-us/azure/vpn-gateway/vpn-gateway-howto-site-to-site-resource-manager-portal) [Point-to-site VPN](https://docs.microsoft.com/en-us/azure/vpn-gateway/vpn-gateway-howto-point-to-site-resource-manager-portal)", + "AuditProcedure": "**Audit from Azure Portal** 1. Open the `Networking` blade for the specific Virtual machine in Azure portal 2. Verify that the `INBOUND PORT RULES` **does not** have a rule for SSH such as - port = `22`, - protocol = `TCP` OR `ANY`, - Source = `Any` OR `Internet` **Audit from Azure CLI** List Network security groups with corresponding non-default Security rules: ``` az network nsg list --query [*].[name,securityRules] ``` Ensure that none of the NSGs have security rule as below ``` access : Allow destinationPortRange : 22 or * or [port range containing 22] direction : Inbound protocol : TCP or * sourceAddressPrefix : * or 0.0.0.0 or /0 or /0 or internet or any ``` **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:** [22730e10-96f6-4aac-ad84-9383d35b5917](https://portal.azure.com/#view/Microsoft_Azure_Policy/PolicyDetailBlade/definitionId/%2Fproviders%2FMicrosoft.Authorization%2FpolicyDefinitions%2F22730e10-96f6-4aac-ad84-9383d35b5917) **- Name:** 'Management ports should be closed on your virtual machines'", + "AdditionalInformation": "", + "References": "https://docs.microsoft.com/en-us/azure/security/azure-security-network-security-best-practices#disable-rdpssh-access-to-azure-virtual-machines:https://learn.microsoft.com/en-us/security/benchmark/azure/mcsb-network-security#ns-1-establish-network-segmentation-boundaries:Express Route: https://docs.microsoft.com/en-us/azure/expressroute/:Site-to-Site VPN: https://docs.microsoft.com/en-us/azure/vpn-gateway/vpn-gateway-howto-site-to-site-resource-manager-portal:Point-to-Site VPN: https://docs.microsoft.com/en-us/azure/vpn-gateway/vpn-gateway-howto-point-to-site-resource-manager-portal", + "DefaultValue": "By default, SSH access from internet is not `enabled`." + } + ] + }, + { + "Id": "7.3", + "Description": "Ensure that UDP access from the Internet is evaluated and restricted", + "Checks": [ + "network_udp_internet_access_restricted" + ], + "Attributes": [ + { + "Section": "7 Networking Services", + "Profile": "Level 1", + "AssessmentStatus": "Automated", + "Description": "Network security groups should be periodically evaluated for port misconfigurations. Where certain ports and protocols may be exposed to the Internet, they should be evaluated for necessity and restricted wherever they are not explicitly required.", + "RationaleStatement": "The potential security problem with broadly exposing UDP services over the Internet is that attackers can use DDoS amplification techniques to reflect spoofed UDP traffic from Azure Virtual Machines. The most common types of these attacks use exposed DNS, NTP, SSDP, SNMP, CLDAP and other UDP-based services as amplification sources for disrupting services of other machines on the Azure Virtual Network or even attack networked devices outside of Azure.", + "ImpactStatement": "Restricting UDP access may impact services that legitimately require UDP traffic.", + "RemediationProcedure": "Where UDP is not explicitly required and narrowly configured for resources attached to the Network Security Group, Internet-level access to your Azure resources should be restricted or eliminated. For internal access to relevant resources, configure an encrypted network tunnel such as: [ExpressRoute](https://docs.microsoft.com/en-us/azure/expressroute/) [Site-to-site VPN](https://docs.microsoft.com/en-us/azure/vpn-gateway/vpn-gateway-howto-site-to-site-resource-manager-portal) [Point-to-site VPN](https://docs.microsoft.com/en-us/azure/vpn-gateway/vpn-gateway-howto-point-to-site-resource-manager-portal)", + "AuditProcedure": "**Audit from Azure Portal** 1. Open the `Networking` blade for the specific Virtual machine in Azure portal 2. Verify that the `INBOUND PORT RULES` **does not** have a rule for UDP such as - protocol = `UDP`, - Source = `Any` OR `Internet` **Audit from Azure CLI** List Network security groups with corresponding non-default Security rules: ``` az network nsg list --query [*].[name,securityRules] ``` Ensure that none of the NSGs have security rule as below ``` access : Allow destinationPortRange : * or [port range containing 53, 123, 161, 389, 1900, or other vulnerable UDP-based services] direction : Inbound protocol : UDP sourceAddressPrefix : * or 0.0.0.0 or /0 or /0 or internet or any ```", + "AdditionalInformation": "", + "References": "https://docs.microsoft.com/en-us/azure/security/fundamentals/network-best-practices#secure-your-critical-azure-service-resources-to-only-your-virtual-networks:https://docs.microsoft.com/en-us/azure/security/fundamentals/ddos-best-practices:https://learn.microsoft.com/en-us/security/benchmark/azure/mcsb-network-security#ns-1-establish-network-segmentation-boundaries:ExpressRoute: https://docs.microsoft.com/en-us/azure/expressroute/:Site-to-site VPN: https://docs.microsoft.com/en-us/azure/vpn-gateway/vpn-gateway-howto-site-to-site-resource-manager-portal:Point-to-site VPN: https://docs.microsoft.com/en-us/azure/vpn-gateway/vpn-gateway-howto-point-to-site-resource-manager-portal", + "DefaultValue": "By default, UDP access from internet is not `enabled`." + } + ] + }, + { + "Id": "7.4", + "Description": "Ensure that HTTP(S) access from the Internet is evaluated and restricted", + "Checks": [ + "network_http_internet_access_restricted" + ], + "Attributes": [ + { + "Section": "7 Networking Services", + "Profile": "Level 1", + "AssessmentStatus": "Automated", + "Description": "Network security groups should be periodically evaluated for port misconfigurations. Where certain ports and protocols may be exposed to the Internet, they should be evaluated for necessity and restricted wherever they are not explicitly required and narrowly configured.", + "RationaleStatement": "The potential security problem with using HTTP(S) over the Internet is that attackers can use various brute force techniques to gain access to Azure resources. Once the attackers gain access, they can use the resource as a launch point for compromising other resources within the Azure tenant.", + "ImpactStatement": "Restricting HTTP(S) access may require proper configuration of web application firewalls and load balancers.", + "RemediationProcedure": "**Remediate from Azure Portal** 1. Go to `Virtual machines`. 2. For each VM, open the `Networking` blade. 3. Click on `Inbound port rules`. 4. Delete the rule with: * Port = 80/443 OR \\[port range containing 80/443\\] * Protocol = TCP OR Any * Source = Any (\\*) OR IP Addresses(0.0.0.0/0) OR Service Tag(Internet) * Action = Allow **Remediate from Azure CLI** 1. Run below command to list network security groups: ``` az network nsg list --subscription --output table ``` 2. For each network security group, run below command to list the rules associated with the specified port: ``` az network nsg rule list --resource-group --nsg-name --query [?destinationPortRange=='80 or 443'] ``` 3. Run the below command to delete the rule with: * Port = 80/443 OR \\[port range containing 80/443\\] * Protocol = TCP OR * * Source = Any (\\*) OR IP Addresses(0.0.0.0/0) OR Service Tag(Internet) * Action = Allow ``` az network nsg rule delete --resource-group --nsg-name --name ```", + "AuditProcedure": "**Audit from Azure Portal** 1. For each VM, open the Networking blade 2. Verify that the INBOUND PORT RULES does not have a rule for HTTP(S) such as - port = `80`/ `443`, - protocol = `TCP`, - Source = `Any` OR `Internet` **Audit from Azure CLI** List Network security groups with corresponding non-default Security rules: ``` az network nsg list --query [*].[name,securityRules] ``` Ensure that none of the NSGs have security rule as below ``` access : Allow destinationPortRange : 80/443 or * or [port range containing 80/443] direction : Inbound protocol : TCP sourceAddressPrefix : * or 0.0.0.0 or /0 or /0 or internet or any ```", + "AdditionalInformation": "", + "References": "Express Route: https://docs.microsoft.com/en-us/azure/expressroute/:Site-to-Site VPN: https://docs.microsoft.com/en-us/azure/vpn-gateway/vpn-gateway-howto-site-to-site-resource-manager-portal:Point-to-Site VPN: https://docs.microsoft.com/en-us/azure/vpn-gateway/vpn-gateway-howto-point-to-site-resource-manager-portal:https://learn.microsoft.com/en-us/security/benchmark/azure/mcsb-network-security#ns-1-establish-network-segmentation-boundaries", + "DefaultValue": "" + } + ] + }, + { + "Id": "7.5", + "Description": "Ensure that network security group flow log retention days is set to greater than or equal to 90", + "Checks": [ + "network_flow_log_more_than_90_days" + ], + "Attributes": [ + { + "Section": "7 Networking Services", + "Profile": "Level 2", + "AssessmentStatus": "Automated", + "Description": "Ensure that virtual network flow logs are retained for greater than or equal to 90 days.", + "RationaleStatement": "Virtual network flow logs provide critical visibility into traffic patterns. Logs can be used to check for anomalies and give insight into suspected breaches.", + "ImpactStatement": "* Virtual network flow logs are charged per gigabyte of network flow logs collected and come with a free tier of 5 GB/month per subscription. * If traffic analytics is enabled with virtual network flow logs, traffic analytics pricing applies at per gigabyte processing rates. * The storage of logs is charged separately, and the cost will depend on the amount of logs and the retention period.", + "RemediationProcedure": "**Remediate from Azure Portal** 1. Go to `Network Watcher`. 1. Under `Logs`, select `Flow logs`. 1. Click `Add filter`. 1. From the `Filter` drop-down menu, select `Flow log type`. 1. From the `Value` drop-down menu, check `Virtual network` only. 1. Click `Apply`. 1. Click the name of a virtual network flow log. 1. Under `Storage Account`, set `Retention days` to `0`, `90`, or a number greater than 90. If `Retention days` is set to `0`, the logs are retained indefinitely with no retention policy. 1. Repeat steps 7 and 8 for each virtual network flow log requiring remediation. **Remediate from Azure CLI** Run the following command update the retention policy for a flow log in a network watcher, setting `retention` to `0`, `90`, or a number greater than 90: ``` az network watcher flow-log update --location --name --retention ``` Repeat for each virtual network flow log requiring remediation.", + "AuditProcedure": "**Audit from Azure Portal** 1. Go to `Network Watcher`. 1. Under `Logs`, select `Flow logs`. 1. Click `Add filter`. 1. From the `Filter` drop-down menu, select `Flow log type`. 1. From the `Value` drop-down menu, check `Virtual network` only. 1. Click `Apply`. 1. Click the name of a virtual network flow log. 1. Under `Storage Account`, ensure that `Retention days` is set to `0`, `90`, or a number greater than 90. If `Retention days` is set to `0`, the logs are retained indefinitely with no retention policy. 1. Repeat steps 7 and 8 for each virtual network flow log. **Audit from Azure CLI** Run the following command to list network watchers: ``` az network watcher list ``` Run the following command to list the name and retention policy of flow logs in a network watcher: ``` az network watcher flow-log list --location --query [*].[name,retentionPolicy] ``` For each flow log, ensure that `days` is set to `0`, `90`, or a number greater than 90. If `days` is set to `0`, the logs are retained indefinitely with no retention policy. Repeat for each network watcher.", + "AdditionalInformation": "As network security group flow logs are on the retirement path, Azure recommends migrating to virtual network flow logs.", + "References": "https://learn.microsoft.com/en-us/azure/network-watcher/vnet-flow-logs-portal:https://learn.microsoft.com/en-us/cli/azure/network/watcher/flow-log", + "DefaultValue": "When a virtual network flow log is created using the Azure CLI, retention days is set to 0 by default. When creating via the Azure Portal, retention days must be specified by the creator." + } + ] + }, + { + "Id": "7.6", + "Description": "Ensure that Network Watcher is 'Enabled' for Azure Regions that are in use", + "Checks": [ + "network_watcher_enabled" + ], + "Attributes": [ + { + "Section": "7 Networking Services", + "Profile": "Level 1", + "AssessmentStatus": "Automated", + "Description": "Enable Network Watcher for physical regions in Azure subscriptions.", + "RationaleStatement": "Network diagnostic and visualization tools available with Network Watcher help users understand, diagnose, and gain insights to the network in Azure.", + "ImpactStatement": "There are additional costs per transaction to run and store network data. For high-volume networks these charges will add up quickly.", + "RemediationProcedure": "Opting out of Network Watcher automatic enablement is a permanent change. Once you opt-out you cannot opt-in without contacting support. To manually enable Network Watcher in each region where you want to use Network Watcher capabilities, follow the steps below. **Remediate from Azure Portal** 1. Use the Search bar to search for and click on the `Network Watcher` service. 1. Click `Create`. 1. Select a `Region` from the drop-down menu. 1. Click `Add`. **Remediate from Azure CLI** ``` az network watcher configure --locations --enabled true --resource-group ```", + "AuditProcedure": "**Audit from Azure Portal** 1. Use the Search bar to search for and click on the `Network Watcher` service. 1. From the Overview menu item, review each Network Watcher listed, and ensure that a network watcher is listed for each region in use by the subscription. **Audit from Azure CLI** ``` az network watcher list --query [].{Location:location,State:provisioningState} -o table ``` This will list all network watchers and their provisioning state. Ensure `provisioningState` is `Succeeded` for each network watcher. ``` az account list-locations --query [?metadata.regionType=='Physical'].{Name:name,DisplayName:regionalDisplayName} -o table ``` This will list all physical regions that exist in the subscription. Compare this list to the previous one to ensure that for each region in use, a network watcher exists with `provisioningState` set to `Succeeded`. **Audit from PowerShell** Get a list of Network Watchers ``` Get-AzNetworkWatcher ``` Make sure each watcher is set with the `ProvisioningState` setting set to `Succeeded` and all `Locations` that are in use by the subscription are using a watcher. **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:** [b6e2945c-0b7b-40f5-9233-7a5323b5cdc6](https://portal.azure.com/#view/Microsoft_Azure_Policy/PolicyDetailBlade/definitionId/%2Fproviders%2FMicrosoft.Authorization%2FpolicyDefinitions%2Fb6e2945c-0b7b-40f5-9233-7a5323b5cdc6) **- Name:** 'Network Watcher should be enabled'", + "AdditionalInformation": "", + "References": "https://docs.microsoft.com/en-us/azure/network-watcher/network-watcher-monitoring-overview:https://learn.microsoft.com/en-us/cli/azure/network/watcher?view=azure-cli-latest:https://learn.microsoft.com/en-us/cli/azure/network/watcher?view=azure-cli-latest#az-network-watcher-configure:https://learn.microsoft.com/en-us/azure/network-watcher/network-watcher-create:https://learn.microsoft.com/en-us/security/benchmark/azure/mcsb-logging-threat-detection#lt-4-enable-network-logging-for-security-investigation:https://azure.microsoft.com/en-ca/pricing/details/network-watcher/", + "DefaultValue": "Network Watcher is automatically enabled. When you create or update a virtual network in your subscription, Network Watcher will be enabled automatically in your Virtual Network's region. There is no impact to your resources or associated charge for automatically enabling Network Watcher." + } + ] + }, + { + "Id": "7.7", + "Description": "Ensure that Public IP addresses are Evaluated on a Periodic Basis", + "Checks": [ + "network_public_ip_shodan" + ], + "Attributes": [ + { + "Section": "7 Networking Services", + "Profile": "Level 1", + "AssessmentStatus": "Manual", + "Description": "Public IP Addresses provide tenant accounts with Internet connectivity for resources contained within the tenant. During the creation of certain resources in Azure, a Public IP Address may be created. All Public IP Addresses within the tenant should be periodically reviewed for accuracy and necessity.", + "RationaleStatement": "Public IP Addresses allocated to the tenant should be periodically reviewed for necessity. Public IP Addresses that are not intentionally assigned and controlled present a publicly facing vector for threat actors and significant risk to the tenant.", + "ImpactStatement": "Regular reviews require administrative effort.", + "RemediationProcedure": "Remediation will vary significantly depending on your organization's security requirements for the resources attached to each individual Public IP address.", + "AuditProcedure": "**Audit from Azure Portal** 1. Open the `All Resources` blade 2. Click on `Add Filter` 3. In the Add Filter window, select the following: Filter: `Type` Operator: `Equals` Value: `Public IP address` 4. Click the `Apply` button 5. For each Public IP address in the list, use Overview (or Properties) to review the `Associated to:` field and determine if the associated resource is still relevant to your tenant environment. If the associated resource is relevant, ensure that additional controls exist to mitigate risk (e.g. Firewalls, VPNs, Traffic Filtering, Virtual Gateway Appliances, Web Application Firewalls, etc.) on all subsequently attached resources. **Audit from Azure CLI** List all Public IP addresses: ``` az network public-ip list ``` For each Public IP address in the output, review the `name` property and determine if the associated resource is still relevant to your tenant environment. If the associated resource is relevant, ensure that additional controls exist to mitigate risk (e.g. Firewalls, VPNs, Traffic Filtering, Virtual Gateway Appliances, Web Application Firewalls, etc.) on all subsequently attached resources.", + "AdditionalInformation": "", + "References": "https://docs.microsoft.com/en-us/cli/azure/network/public-ip?view=azure-cli-latest:https://learn.microsoft.com/en-us/security/benchmark/azure/mcsb-network-security", + "DefaultValue": "During Virtual Machine and Application creation, a setting may create and attach a public IP." + } + ] + }, + { + "Id": "7.8", + "Description": "Ensure that virtual network flow log retention days is set to greater than or equal to 90", + "Checks": [ + "network_flow_log_more_than_90_days" + ], + "Attributes": [ + { + "Section": "7 Networking Services", + "Profile": "Level 2", + "AssessmentStatus": "Automated", + "Description": "Ensure that virtual network flow logs are retained for greater than or equal to 90 days.", + "RationaleStatement": "Virtual network flow logs provide critical visibility into traffic patterns. Logs can be used to check for anomalies and give insight into suspected breaches.", + "ImpactStatement": "* Virtual network flow logs are charged per gigabyte of network flow logs collected and come with a free tier of 5 GB/month per subscription. * If traffic analytics is enabled with virtual network flow logs, traffic analytics pricing applies at per gigabyte processing rates. * The storage of logs is charged separately, and the cost will depend on the amount of logs and the retention period.", + "RemediationProcedure": "**Remediate from Azure Portal** 1. Go to `Network Watcher`. 1. Under `Logs`, select `Flow logs`. 1. Click `Add filter`. 1. From the `Filter` drop-down menu, select `Flow log type`. 1. From the `Value` drop-down menu, check `Virtual network` only. 1. Click `Apply`. 1. Click the name of a virtual network flow log. 1. Under `Storage Account`, set `Retention days` to `0`, `90`, or a number greater than 90. If `Retention days` is set to `0`, the logs are retained indefinitely with no retention policy. 1. Repeat steps 7 and 8 for each virtual network flow log requiring remediation. **Remediate from Azure CLI** Run the following command update the retention policy for a flow log in a network watcher, setting `retention` to `0`, `90`, or a number greater than 90: ``` az network watcher flow-log update --location --name --retention ``` Repeat for each virtual network flow log requiring remediation.", + "AuditProcedure": "**Audit from Azure Portal** 1. Go to `Network Watcher`. 1. Under `Logs`, select `Flow logs`. 1. Click `Add filter`. 1. From the `Filter` drop-down menu, select `Flow log type`. 1. From the `Value` drop-down menu, check `Virtual network` only. 1. Click `Apply`. 1. Click the name of a virtual network flow log. 1. Under `Storage Account`, ensure that `Retention days` is set to `0`, `90`, or a number greater than 90. If `Retention days` is set to `0`, the logs are retained indefinitely with no retention policy. 1. Repeat steps 7 and 8 for each virtual network flow log. **Audit from Azure CLI** Run the following command to list network watchers: ``` az network watcher list ``` Run the following command to list the name and retention policy of flow logs in a network watcher: ``` az network watcher flow-log list --location --query [*].[name,retentionPolicy] ``` For each flow log, ensure that `days` is set to `0`, `90`, or a number greater than 90. If `days` is set to `0`, the logs are retained indefinitely with no retention policy. Repeat for each network watcher.", + "AdditionalInformation": "As network security group flow logs are on the retirement path, Azure recommends migrating to virtual network flow logs.", + "References": "https://learn.microsoft.com/en-us/azure/network-watcher/vnet-flow-logs-portal:https://learn.microsoft.com/en-us/cli/azure/network/watcher/flow-log", + "DefaultValue": "When a virtual network flow log is created using the Azure CLI, retention days is set to 0 by default. When creating via the Azure Portal, retention days must be specified by the creator." + } + ] + }, + { + "Id": "7.9", + "Description": "Ensure 'Authentication type' is set to 'Azure Active Directory' only for Azure VPN Gateway point-to-site configuration", + "Checks": [], + "Attributes": [ + { + "Section": "7 Networking Services", + "Profile": "Level 2", + "AssessmentStatus": "Manual", + "Description": "Enable only 'Azure Active Directory' (Microsoft Entra ID) authentication for Azure VPN Gateway point-to-site connections.", + "RationaleStatement": "Microsoft Entra ID authentication provides strong security and centralized identity management, and reduces risks associated with static credentials and certificate management.", + "ImpactStatement": "Azure VPN Gateways incur hourly charges, with additional costs for point-to-site connections and data transfer. Pricing varies by SKU and usage. Refer to https://azure.microsoft.com/en-us/pricing/details/vpn-gateway/ for details.", + "RemediationProcedure": "**Remediate from Azure Portal** 1. Go to `Virtual network gateways`. 2. Under `VPN gateway`, click `VPN gateways`. 3. Click the name of a VPN gateway. 4. Under `Settings`, click `Point-to-site configuration`. 5. Ensure `Authentication type` click to expand the drop-down menu. 6. Check the box next to `Azure Active Directory`, and uncheck the boxes next to `Azure certificate` and `RADIUS authentication`. 7. Provide a `Tenant`, `Audience`, and `Issuer` for the Azure Active Directory configuration. 8. Click `Save`. 9. Repeat steps 1-8 for each VPN gateway requiring remediation.", + "AuditProcedure": "**Audit from Azure Portal** 1. Go to `Virtual network gateways`. 2. Under `VPN gateway`, click `VPN gateways`. 3. Click the name of a VPN gateway. 4. Under `Settings`, click `Point-to-site configuration`. 5. Ensure `Authentication type` is set to `Azure Active Directory` only. 6. Repeat steps 1-5 for each VPN gateway. **Audit from Azure Policy** - **Policy ID:** 21a6bc25-125e-4d13-b82d-2e19b7208ab7 - **Name:** 'VPN gateways should use only Azure Active Directory (Azure AD) authentication for point-to-site users'", + "AdditionalInformation": "", + "References": "https://learn.microsoft.com/en-us/azure/vpn-gateway/vpn-gateway-about-vpngateways:https://learn.microsoft.com/en-us/azure/vpn-gateway/point-to-site-entra-gateway:https://learn.microsoft.com/en-us/azure/vpn-gateway/openvpn-azure-ad-tenant", + "DefaultValue": "'Authentication type' is selected during creation of point-to-site configuration." + } + ] + }, + { + "Id": "7.10", + "Description": "Ensure Azure Web Application Firewall (WAF) is enabled on Azure Application Gateway", + "Checks": [], + "Attributes": [ + { + "Section": "7 Networking Services", + "Profile": "Level 2", + "AssessmentStatus": "Automated", + "Description": "Azure Web Application Firewall helps protect applications from common exploits and attacks by inspecting and filtering incoming traffic.", + "RationaleStatement": "Using Azure Web Application Firewall with Azure Application Gateway reduces exposure to external threats by mitigating attacks on public facing applications.", + "ImpactStatement": "The WAF V2 tier for Azure Application Gateways costs more than the Basic and Standard V2 tiers. Pricing includes a fixed hourly charge plus a charge per capacity-unit hour. Refer to https://azure.microsoft.com/en-gb/pricing/details/application-gateway/ for details.", + "RemediationProcedure": "**Note:** Basic tier application gateways cannot be upgraded to the WAF V2 tier. Create a new WAF V2 tier application gateway to replace a Basic tier application gateway. **Remediate from Azure Portal** To remediate a Standard V2 tier application gateway: 1. Go to `Application gateways`. 2. Click `Add filter`. 3. From the `Filter` drop-down menu, select `SKU size`. 4. Check the box next to `Standard_v2` only. 5. Click `Apply`. 6. Click the name of an application gateway. 7. Under `Settings`, click `Web application firewall`. 8. Under `Configure`, next to `Tier`, click `WAF V2`. 9. Select an existing or create a new WAF policy. 10. Click `Save`. 11. Repeat steps 1-10 for each Standard V2 tier application gateway requiring remediation.", + "AuditProcedure": "**Audit from Azure Portal** 1. Go to `Application gateways`. 2. Click the name of an application gateway. 3. In the `Overview`, under `Essentials`, ensure `Tier` is set to `WAF V2`. 4. Repeat steps 1-3 for each application gateway. **Audit from Azure CLI** Run the following command to list application gateways: ``` az network application-gateway list ``` For each application gateway, run the following command to get the firewall policy id: ``` az network application-gateway show --resource-group --name --query firewallPolicy.id ``` Ensure a firewall policy id is returned. **Audit from Azure Policy** - **Policy ID:** 564feb30-bf6a-4854-b4bb-0d2d2d1e6c66 - **Name:** 'Web Application Firewall (WAF) should be enabled for Application Gateway'", + "AdditionalInformation": "", + "References": "https://learn.microsoft.com/en-us/azure/application-gateway/features:https://learn.microsoft.com/en-us/cli/azure/network/application-gateway:https://azure.microsoft.com/en-us/pricing/details/application-gateway", + "DefaultValue": "Azure Web Application Firewall is enabled by default for the WAF V2 tier of Azure Application Gateway. It is not available in the Basic tier. Application gateways deployed using the Standard V2 tier can be upgraded to the WAF V2 tier to enable Azure Web Application Firewall." + } + ] + }, + { + "Id": "7.11", + "Description": "Ensure subnets are associated with network security groups", + "Checks": [], + "Attributes": [ + { + "Section": "7 Networking Services", + "Profile": "Level 1", + "AssessmentStatus": "Automated", + "Description": "Protect subnet resources by ensuring subnets are associated with network security groups, which can filter inbound and outbound traffic using security rules.", + "RationaleStatement": "Unprotected subnets can expose resources to unauthorized access.", + "ImpactStatement": "Minor administrative effort is required to ensure subnets are associated with network security groups. There is no cost to create or use network security groups.", + "RemediationProcedure": "**Remediate from Azure Portal** 1. Go to `Virtual networks`. 2. Click the name of a virtual network. 3. Under `Settings`, click `Subnets`. 4. Click the name of a subnet. 5. Under `Security`, next to `Network security group`, click `None` to display the drop-down menu. 6. Select a network security group. 7. Click `Save`. 8. Repeat steps 1-7 for each virtual network and subnet requiring remediation. **Remediate from Azure CLI** For each subnet requiring remediation, run the following command to associate it with a network security group: ``` az network vnet subnet update --resource-group --vnet-name --name --network-security-group ```", + "AuditProcedure": "**Audit from Azure Portal** 1. Go to `Virtual networks`. 2. Click the name of a virtual network. 3. Under `Settings`, click `Subnets`. 4. Click the name of a subnet. 5. Under `Security`, ensure `Network security group` is not set to `None`. 6. Repeat steps 1-5 for each virtual network and subnet. **Audit from Azure CLI** Run the following command to list virtual networks: ``` az network vnet list ``` For each virtual network, run the following command to list subnets: ``` az network vnet show --resource-group --name --query subnets ``` For each subnet, run the following command to get the network security group id: ``` az network vnet subnet show --resource-group --vnet-name --name --query networkSecurityGroup.id ``` Ensure a network security group id is returned. **Audit from Azure Policy** - **Policy ID:** e71308d3-144b-4262-b144-efdc3cc90517 - **Name:** 'Subnets should be associated with a Network Security Group'", + "AdditionalInformation": "", + "References": "https://learn.microsoft.com/en-us/azure/virtual-network/network-security-groups-overview:https://learn.microsoft.com/en-us/cli/azure/network/vnet", + "DefaultValue": "By default, a subnet is not associated with a network security group." + } + ] + }, + { + "Id": "7.12", + "Description": "Ensure the SSL policy's 'Min protocol version' is set to 'TLSv1_2' or higher on Azure Application Gateway", + "Checks": [], + "Attributes": [ + { + "Section": "7 Networking Services", + "Profile": "Level 1", + "AssessmentStatus": "Automated", + "Description": "The TLS (Transport Layer Security) protocol secures the transmission of data over the internet using standard encryption technology. Application gateways use TLS 1.2 for the Min protocol version by default and allow for the use of TLS versions 1.0, 1.1, and 1.3. NIST strongly suggests the use of TLS 1.2 and recommends the adoption of TLS 1.3.", + "RationaleStatement": "TLS 1.0 and 1.1 are outdated and vulnerable to security risks. Since TLS 1.2 and TLS 1.3 provide enhanced security and improved performance, it is highly recommended to use TLS 1.2 or higher whenever possible.", + "ImpactStatement": "Using the latest TLS version may affect compatibility with clients and backend services.", + "RemediationProcedure": "**Remediate from Azure Portal** 1. Go to `Application gateways`. 2. Click the name of an application gateway. 3. Under `Settings`, click `Listeners`. 4. Under `SSL Policy`, next to the Selected SSL Policy name, click `change`. 5. Select an appropriate SSL policy with a `Min protocol version` of `TLSv1_2` or higher. 6. Click `Save`. 7. Repeat steps 1-6 for each application gateway requiring remediation. **Remediate from Azure CLI** Run the following command to list available SSL policy options: ``` az network application-gateway ssl-policy list-options ``` Run the following command to list available predefined SSL policies: ``` az network application-gateway ssl-policy predefined list ``` For each application gateway requiring remediation, run the following command to set a predefined SSL policy: ``` az network application-gateway ssl-policy set --resource-group --gateway-name --name --policy-type Predefined ``` Alternatively, run the following command to set a custom SSL policy: ``` az network application-gateway ssl-policy set --resource-group --gateway-name --policy-type Custom --min-protocol-version --cipher-suites ```", + "AuditProcedure": "**Audit from Azure Portal** 1. Go to `Application gateways`. 2. Click the name of an application gateway. 3. Under `Settings`, click `Listeners`. 4. Under `SSL Policy`, ensure `Min protocol version` is set to `TLSv1_2` or higher. 5. Repeat steps 1-4 for each application gateway. **Audit from Azure CLI** Run the following command to list application gateways: ``` az network application-gateway list ``` For each application gateway, run the following command to get the SSL policy: ``` az network application-gateway ssl-policy show --resource-group --gateway-name ``` For each SSL policy, run the following command to get the minProtocolVersion: ``` az network application-gateway ssl-policy predefined show --name --query minProtocolVersion ``` Ensure `TLSv1_2` or higher is returned.", + "AdditionalInformation": "", + "References": "https://learn.microsoft.com/en-us/azure/application-gateway/application-gateway-ssl-policy-overview:https://learn.microsoft.com/en-us/cli/azure/network/application-gateway", + "DefaultValue": "Min protocol version is set to TLSv1_2 by default." + } + ] + }, + { + "Id": "7.13", + "Description": "Ensure 'HTTP2' is set to 'Enabled' on Azure Application Gateway", + "Checks": [], + "Attributes": [ + { + "Section": "7 Networking Services", + "Profile": "Level 1", + "AssessmentStatus": "Automated", + "Description": "Enable HTTP/2 for improved performance, efficiency, and security. HTTP/2 protocol support is available to clients that connect to application gateway listeners only. Communication with backend server pools is always HTTP/1.1.", + "RationaleStatement": "Enabling HTTP/2 supports use of modern encrypted connections.", + "ImpactStatement": "Clients and backend services that do not support HTTP/2 will fall back to HTTP/1.1.", + "RemediationProcedure": "**Remediate from Azure Portal** 1. Go to `Application gateways`. 2. Click the name of an application gateway. 3. Under `Settings`, click `Configuration`. 4. Under `HTTP2`, click `Enabled`. 5. Click `Save`. 6. Repeat steps 1-5 for each application gateway requiring remediation. **Remediate from Azure CLI** For each application gateway requiring remediation, run the following command to enable HTTP2: ``` az network application-gateway update --resource-group --name --http2 Enabled ``` **Remediate from PowerShell** Run the following command to get the application gateway in a resource group with a given name: ``` $gateway = Get-AzApplicationGateway -ResourceGroupName -Name ``` Run the following command to enable HTTP2: ``` $gateway.EnableHttp2 = $true ``` Run the following command to apply the update: ``` Set-AzApplicationGateway -ApplicationGateway $gateway ``` Repeat for each application gateway requiring remediation.", + "AuditProcedure": "**Audit from Azure Portal** 1. Go to `Application gateways`. 2. Click the name of an application gateway. 3. Under `Settings`, click `Configuration`. 4. Ensure `HTTP2` is set to `Enabled`. 5. Repeat steps 1-4 for each application gateway. **Audit from Azure CLI** Run the following command to list application gateways: ``` az network application-gateway list ``` For each application gateway, run the following command to get the HTTP2 setting: ``` az network application-gateway show --resource-group --name --query enableHttp2 ``` Ensure `true` is returned. **Audit from PowerShell** Run the following command to list application gateways: ``` Get-AzApplicationGateway ``` Run the following command to get the application gateway in a resource group with a given name: ``` $gateway = Get-AzApplicationGateway -ResourceGroupName -Name ``` Run the following command to get the HTTP2 setting: ``` $gateway.EnableHttp2 ``` Ensure that `True` is returned. Repeat for each application gateway.", + "AdditionalInformation": "", + "References": "https://learn.microsoft.com/en-us/azure/application-gateway/features#websocket-and-http2-traffic:https://learn.microsoft.com/en-us/cli/azure/network/application-gateway:https://learn.microsoft.com/en-us/powershell/module/az.network/get-azapplicationgateway:https://learn.microsoft.com/en-us/powershell/module/az.network/set-azapplicationgateway", + "DefaultValue": "HTTP2 is enabled by default." + } + ] + }, + { + "Id": "7.14", + "Description": "Ensure request body inspection is enabled in Azure Web Application Firewall policy on Azure Application Gateway", + "Checks": [], + "Attributes": [ + { + "Section": "7 Networking Services", + "Profile": "Level 2", + "AssessmentStatus": "Automated", + "Description": "Enable request body inspection so that the Web Application Firewall evaluates the contents of HTTP message bodies for potential threats.", + "RationaleStatement": "Enabling request body inspection strengthens security by allowing the Web Application Firewall to detect common attacks, such as SQL injection and cross-site scripting.", + "ImpactStatement": "Minor performance impact on the Web Application Firewall. Additional effort may be required to monitor findings.", + "RemediationProcedure": "**Remediate from Azure Portal** 1. Go to `Application gateways`. 2. Click the name of an application gateway. 3. Under `Settings`, click `Web application firewall`. 4. Under `Associated web application firewall policy`, click the policy name. 5. Under `Settings`, click `Policy settings`. 6. Check the box next to `Enforce request body inspection`. 7. Click `Save`. 8. Repeat steps 1-7 for each application gateway and firewall policy requiring remediation. **Remediate from Azure CLI** For each firewall policy requiring remediation, run the following command to enable request body inspection: ``` az network application-gateway waf-policy update --ids --policy-settings request-body-check=true ```", + "AuditProcedure": "**Audit from Azure Portal** 1. Go to `Application gateways`. 2. Click the name of an application gateway. 3. Under `Settings`, click `Web application firewall`. 4. Under `Associated web application firewall policy`, click the policy name. 5. Under `Settings`, click `Policy settings`. 6. Ensure the box next to `Enforce request body inspection` is checked. 7. Repeat steps 1-6 for each application gateway. **Audit from Azure CLI** Run the following command to list application gateways: ``` az network application-gateway list ``` For each application gateway, run the following command to get the firewall policy id: ``` az network application-gateway show --resource-group --name --query firewallPolicy.id ``` For each firewall policy, run the following command to get the request body inspection setting: ``` az network application-gateway waf-policy show --ids --query policySettings.requestBodyCheck ``` Ensure `true` is returned. **Audit from Azure Policy** - **Policy ID:** ca85ef9a-741d-461d-8b7a-18c2da82c666 - **Name:** 'Azure Web Application Firewall on Azure Application Gateway should have request body inspection enabled'", + "AdditionalInformation": "", + "References": "https://learn.microsoft.com/en-gb/azure/web-application-firewall/ag/application-gateway-waf-request-size-limits#request-body-inspection:https://learn.microsoft.com/en-us/cli/azure/network/application-gateway:https://learn.microsoft.com/en-us/cli/azure/network/application-gateway/waf-policy", + "DefaultValue": "Request body inspection is enabled by default on Azure Application Gateways with Web Application Firewall." + } + ] + }, + { + "Id": "7.15", + "Description": "Ensure bot protection is enabled in Azure Web Application Firewall policy on Azure Application Gateway", + "Checks": [], + "Attributes": [ + { + "Section": "7 Networking Services", + "Profile": "Level 2", + "AssessmentStatus": "Automated", + "Description": "Enable bot protection on the Web Application Firewall to block or log requests from known malicious IP addresses identified through the Microsoft Threat Intelligence feed.", + "RationaleStatement": "Internet traffic from bots can scrape, scan, and search for application vulnerabilities. Enabling bot protection stops requests from known malicious IP addresses and enhances the overall security of your application by reducing exposure to automated attacks.", + "ImpactStatement": "May require monitoring to identify false positives.", + "RemediationProcedure": "**Remediate from Azure Portal** 1. Go to `Application gateways`. 2. Click the name of an application gateway. 3. Under `Settings`, click `Web application firewall`. 4. Under `Associated web application firewall policy`, click the policy name. 5. Under `Settings`, click `Managed rules`. 6. Click `Assign`. 7. Under `Bot Management ruleset`, click to display the drop-down menu. 8. Select a `Microsoft_BotManagerRuleSet`. 9. Click `Save`. 10. Click `X` to close the panel. 11. Repeat steps 1-10 for each application gateway and firewall policy requiring remediation. **Remediate from Azure CLI** For each firewall policy requiring remediation, run the following command to enable bot protection: ``` az network application-gateway waf-policy managed-rule rule-set add --resource-group --policy-name --type Microsoft_BotManagerRuleSet --version <0.1|1.0|1.1> ```", + "AuditProcedure": "**Audit from Azure Portal** 1. Go to `Application gateways`. 2. Click the name of an application gateway. 3. Under `Settings`, click `Web application firewall`. 4. Under `Associated web application firewall policy`, click the policy name. 5. Under `Settings`, click `Managed rules`. 6. Ensure a `Rule Id` containing `Microsoft_BotManagerRuleSet` is listed. 7. Click the `>` to expand the row. 8. Ensure the `Status` for `Malicious Bots` is set to `Enabled`. 9. Repeat steps 1-8 for each application gateway. **Audit from Azure CLI** Run the following command to list application gateways: ``` az network application-gateway list ``` For each application gateway, run the following command to get the firewall policy id: ``` az network application-gateway show --resource-group --name --query firewallPolicy.id ``` For each firewall policy, run the following command to get the managed rule sets: ``` az network application-gateway waf-policy show --ids --query managedRules.managedRuleSets ``` Ensure a managed rule set with `ruleSetType` of `Microsoft_BotManagerRuleSet` is returned, and that no `ruleGroupOverrides` for `ruleGroupName` `KnownBadBots` with `state` `Disabled` are returned. **Audit from Azure Policy** - **Policy ID:** ebea0d86-7fbd-42e3-8a46-27e7568c2525 - **Name:** 'Bot Protection should be enabled for Azure Application Gateway WAF'", + "AdditionalInformation": "", + "References": "https://learn.microsoft.com/en-us/azure/web-application-firewall/ag/bot-protection-overview:https://learn.microsoft.com/en-us/azure/web-application-firewall/ag/bot-protection:https://learn.microsoft.com/en-us/cli/azure/network/application-gateway:https://learn.microsoft.com/en-us/cli/azure/network/application-gateway/waf-policy:https://learn.microsoft.com/en-us/cli/azure/network/application-gateway/waf-policy/managed-rule/rule-set", + "DefaultValue": "Bot protection is disabled by default on Azure Application Gateways with Web Application Firewall." + } + ] + }, + { + "Id": "7.16", + "Description": "Ensure Azure Network Security Perimeter is used to secure Azure platform-as-a-service resources", + "Checks": [], + "Attributes": [ + { + "Section": "7 Networking Services", + "Profile": "Level 2", + "AssessmentStatus": "Manual", + "Description": "Azure Network Security Perimeter creates a logical boundary around Azure platform-as-a-service (PaaS) resources outside of virtual networks. By default, the network security perimeter denies public access to associated PaaS resources, with the ability to define explicit rules for inbound and outbound traffic.", + "RationaleStatement": "Network security perimeter denies public access to PaaS resources, reducing exposure and mitigating data exfiltration risks.", + "ImpactStatement": "Implementation requires administrative effort to configure and maintain network security perimeter profiles and resource assignments. Azure does not list any additional charges for using network security perimeters.", + "RemediationProcedure": "**Remediate from Azure Portal** Create and associate PaaS resources with a new network security perimeter: 1. Go to `Network Security Perimeters`. 2. Click `+ Create`. 3. Select a `Subscription` and `Resource group`, provide a `Name`, select a `Region`, and provide a `Profile name`. 4. Click `Next`. 5. Click `+ Add`. 6. Check the box next to a PaaS resource to associate it with the network security perimeter. 7. Click `Select`. 8. Click `Next`. 9. Configure appropriate `Inbound access rules` for your organization. 10. Click `Next`. 11. Configure appropriate `Outbound access rules` for your organization. 12. Click `Review + create`. 13. Click `Create`. **Remediate from Azure CLI** Use `az network perimeter profile list` or `az network perimeter profile create` to list existing or create a new network security perimeter profile. For each PaaS resource requiring association with a network security perimeter, run the following command: ``` az network perimeter association create --resource-group --perimeter-name --association-name --private-link-resource \"{id:}\" --profile \"{}\" ```", + "AuditProcedure": "**Audit from Azure Portal** 1. Go to `Resource groups`. 2. Click the name of a resource group. 3. Take note of PaaS resources. 4. Go to `Network Security Perimeters`. 5. Click the name of a network security perimeter. 6. Under `Settings`, click `Associated resources`. 7. Take note of the associated resources. 8. Repeat steps 1-7 and ensure each PaaS resource is associated with a network security perimeter. **Audit from Azure CLI** Run the following command to list resource groups: ``` az group list ``` For each resource group, run the following command to list resources: ``` az resource list --resource-group ``` Take note of PaaS resources. For each resource group, run the following command to list network security perimeters: ``` az network perimeter list --resource-group ``` For each network security perimeter, run the following command to list resources: ``` az network perimeter association list --resource-group --perimeter-name ``` Ensure each PaaS resource is associated with a network security perimeter.", + "AdditionalInformation": "The current list of resources that can be associated with a network security perimeter are as follows: Azure Monitor, Azure AI Search, Cosmos DB, Event Hubs, Key Vault, SQL DB, Storage, Azure OpenAI Service. While network security perimeter is generally available, Cosmos DB, SQL DB, and Azure OpenAI Service are in public preview.", + "References": "https://learn.microsoft.com/en-us/azure/private-link/network-security-perimeter-concepts:https://learn.microsoft.com/en-us/azure/private-link/create-network-security-perimeter-portal:https://learn.microsoft.com/en-us/cli/azure/group:https://learn.microsoft.com/en-us/cli/azure/resource:https://learn.microsoft.com/en-us/cli/azure/network/perimeter", + "DefaultValue": "PaaS resources are not associated with a network security perimeter by default." + } + ] + }, + { + "Id": "8.1.1.1", + "Description": "Ensure Microsoft Defender CSPM is set to 'On'", + "Checks": [], + "Attributes": [ + { + "Section": "8 Security Services", + "SubSection": "8.1 Microsoft Defender for Cloud", + "Profile": "Level 2", + "AssessmentStatus": "Automated", + "Description": "Enable Microsoft Defender CSPM to continuously assess cloud resources for security misconfigurations, compliance risks, and exposure to threats.", + "RationaleStatement": "Microsoft Defender CSPM provides detailed visibility into the security state of assets and workloads and offers hardening guidance to help improve security posture.", + "ImpactStatement": "Enabling Microsoft Defender CSPM incurs hourly charges for each billable compute, database, and storage resource. This can lead to significant costs in larger environments. Careful planning and cost analysis are recommended before enabling the service. Refer to https://azure.microsoft.com/en-us/pricing/details/defender-for-cloud/#pricing for pricing information.", + "RemediationProcedure": "**Remediate from Azure Portal** 1. Go to `Microsoft Defender for Cloud`. 2. Under `Management`, click `Environment settings`. 3. Click the name of a subscription. 4. Select the `Defender plans` blade. 5. Under `Cloud Security Posture Management (CSPM)`, in the row for `Defender CSPM`, set the toggle switch for `Status` to `On`. 6. Click `Save`. **Remediate from Azure CLI** Run the following command to enable Defender CSPM: ``` az security pricing create --name CloudPosture --tier Standard --extensions name=ApiPosture isEnabled=true ``` **Remediate from PowerShell** Run the following command to enable Defender CSPM: ``` Set-AzSecurityPricing -Name CloudPosture -PricingTier Standard -Extension '[{\"name\":\"ApiPosture\",\"isEnabled\":\"True\"}]' ```", + "AuditProcedure": "**Audit from Azure Portal** 1. Go to `Microsoft Defender for Cloud`. 2. Under `Management`, click `Environment settings`. 3. Click the name of a subscription. 4. Select the `Defender plans` blade. 5. Under `Cloud Security Posture Management (CSPM)`, in the row for `Defender CSPM`, ensure `Status` is set to `On`. **Audit from Azure CLI** Run the following command to get the CloudPosture plan pricing tier: ``` az security pricing show --name CloudPosture --query pricingTier ``` Ensure `Standard` is returned. **Audit from PowerShell** Run the following command to get the CloudPosture plan pricing tier: ``` Get-AzSecurityPricing -Name CloudPosture | Select-Object PricingTier ``` Ensure `Standard` is returned. **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:** [1f90fc71-a595-4066-8974-d4d0802e8ef0](https://portal.azure.com/#view/Microsoft_Azure_Policy/PolicyDetailBlade/definitionId/%2Fproviders%2FMicrosoft.Authorization%2FpolicyDefinitions%2F1f90fc71-a595-4066-8974-d4d0802e8ef0) **- Name:** 'Microsoft Defender CSPM should be enabled'", + "AdditionalInformation": "", + "References": "https://learn.microsoft.com/en-us/azure/defender-for-cloud/concept-cloud-security-posture-management:https://learn.microsoft.com/en-us/azure/defender-for-cloud/tutorial-enable-cspm-plan:https://azure.microsoft.com/en-us/pricing/details/defender-for-cloud/#pricing:https://learn.microsoft.com/en-us/cli/azure/security/pricing:https://learn.microsoft.com/en-us/powershell/module/az.security/get-azsecuritypricing:https://learn.microsoft.com/en-us/powershell/module/az.security/set-azsecuritypricing", + "DefaultValue": "Defender CSPM is disabled by default." + } + ] + }, + { + "Id": "8.1.2.1", + "Description": "Ensure Microsoft Defender for APIs is set to 'On'", + "Checks": [ + "defender_ensure_defender_for_app_services_is_on" + ], + "Attributes": [ + { + "Section": "8 Security Services", + "SubSection": "8.1 Microsoft Defender for Cloud", + "Profile": "Level 1", + "AssessmentStatus": "Automated", + "Description": "Turning on Microsoft Defender for App Service enables threat detection for App Service, providing threat intelligence, anomaly detection, and behavior analytics in the Microsoft Defender for Cloud.", + "RationaleStatement": "Enabling Microsoft Defender for App Service allows for greater defense-in-depth, with threat detection provided by the Microsoft Security Response Center (MSRC).", + "ImpactStatement": "Turning on Microsoft Defender for App Service incurs an additional cost per resource.", + "RemediationProcedure": "**Remediate from Azure Portal** 1. Go to `Microsoft Defender for Cloud` 2. Under `Management`, select `Environment Settings` 3. Click on the subscription name 4. Select `Defender plans` 5. Set `App Service` Status to `On` 6. Select `Save` **Remediate from Azure CLI** Run the following command: ``` az security pricing create -n Appservices --tier 'standard' ``` **Remediate from PowerShell** Run the following command: ``` Set-AzSecurityPricing -Name AppServices -PricingTier Standard ```", + "AuditProcedure": "**Audit from Azure Portal** 1. Go to `Microsoft Defender for Cloud` 2. Under `Management`, select `Environment Settings` 3. Click on the subscription name 4. Select `Defender plans` 5. Ensure Status is `On` for `App Service` **Audit from Azure CLI** Run the following command: ``` az security pricing show -n AppServices ``` Ensure `-PricingTier` is set to `Standard` **Audit from PowerShell** Run the following command: ``` Get-AzSecurityPricing -Name 'AppServices' |Select-Object Name,PricingTier ``` Ensure the `-PricingTier` is set to `Standard` **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:** [2913021d-f2fd-4f3d-b958-22354e2bdbcb](https://portal.azure.com/#view/Microsoft_Azure_Policy/PolicyDetailBlade/definitionId/%2Fproviders%2FMicrosoft.Authorization%2FpolicyDefinitions%2F2913021d-f2fd-4f3d-b958-22354e2bdbcb) **- Name:** 'Azure Defender for App Service should be enabled'", + "AdditionalInformation": "", + "References": "https://docs.microsoft.com/en-us/azure/security-center/security-center-detection-capabilities:https://docs.microsoft.com/en-us/rest/api/securitycenter/pricings/list:https://docs.microsoft.com/en-us/rest/api/securitycenter/pricings/update:https://docs.microsoft.com/en-us/powershell/module/az.security/get-azsecuritypricing:https://learn.microsoft.com/en-us/security/benchmark/azure/mcsb-logging-threat-detection#lt-1-enable-threat-detection-capabilities", + "DefaultValue": "By default, Microsoft Defender plan is off." + } + ] + }, + { + "Id": "8.1.3.1", + "Description": "Ensure that Defender for Servers is set to 'On'", + "Checks": [ + "defender_ensure_defender_for_server_is_on" + ], + "Attributes": [ + { + "Section": "8 Security Services", + "SubSection": "8.1 Microsoft Defender for Cloud", + "Profile": "Level 1", + "AssessmentStatus": "Automated", + "Description": "The Defender for Servers plan in Microsoft Defender for Cloud reduces security risk by providing actionable recommendations to improve and remediate machine security posture. Defender for Servers also helps to protect machines against real-time security threats and attacks. Defender for Servers offers two paid plans: **Plan 1** The following components are enabled by default: - Log Analytics agent (deprecated) - Endpoint protection Plan 1 also offers the following components, disabled by default: - Vulnerability assessment for machines - Guest Configuration agent (preview) **Plan 2** The following components are enabled by default: - Log Analytics agent (deprecated) - Vulnerability assessment for machines - Endpoint protection - Agentless scanning for machines Plan 2 also offers the following components, disabled by default: - Guest Configuration agent (preview) - File Integrity Monitoring", + "RationaleStatement": "Enabling Defender for Servers allows for greater defense-in-depth, with threat detection provided by the Microsoft Security Response Center (MSRC).", + "ImpactStatement": "Enabling Defender for Servers in Microsoft Defender for Cloud incurs an additional cost per resource. Refer to https://azure.microsoft.com/en-us/pricing/details/defender-for-cloud/ and https://azure.microsoft.com/en-us/pricing/calculator/ to estimate potential costs. - Plan 1: Subscription only - Plan 2: Subscription and workspace", + "RemediationProcedure": "**Remediate from Azure Portal** 1. Go to `Microsoft Defender for Cloud`. 1. Under `Management`, select `Environment settings`. 1. Click on a subscription name. 1. Click `Defender plans` in the left pane. 1. Under `Cloud Workload Protection (CWP)`, locate `Servers` in the Plan column, set Status to `On`. 1. Select `Save`. 1. Repeat steps 1-6 for each subscription requiring remediation. **Remediate from Azure CLI** Run the following command: ``` az security pricing create -n VirtualMachines --tier 'standard' ``` **Remediate from PowerShell** Run the following command: ``` Set-AzSecurityPricing -Name 'VirtualMachines' -PricingTier 'Standard' ```", + "AuditProcedure": "**Audit from Azure Portal** 1. Go to `Microsoft Defender for Cloud`. 1. Under `Management`, select `Environment settings`. 1. Click on a subscription name. 1. Select `Defender plans` in the left pane. 1. Under `Cloud Workload Protection (CWP)`, locate `Servers` in the Plan column, ensure Status is set to `On`. 1. Repeat steps 1-5 for each subscription. **Audit from Azure CLI** Run the following command: ``` az security pricing show -n VirtualMachines --query pricingTier ``` If the tenant is licensed and enabled, the output will indicate `Standard`. **Audit from PowerShell** Run the following command: ``` Get-AzSecurityPricing -Name 'VirtualMachines' |Select-Object Name,PricingTier ``` If the tenant is licensed and enabled, the `-PricingTier` parameter will indicate `Standard`. **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:** [4da35fc9-c9e7-4960-aec9-797fe7d9051d](https://portal.azure.com/#view/Microsoft_Azure_Policy/PolicyDetailBlade/definitionId/%2Fproviders%2FMicrosoft.Authorization%2FpolicyDefinitions%2F4da35fc9-c9e7-4960-aec9-797fe7d9051d) **- Name:** 'Azure Defender for servers should be enabled'", + "AdditionalInformation": "", + "References": "https://learn.microsoft.com/en-us/azure/defender-for-cloud/defender-for-servers-overview:https://learn.microsoft.com/en-us/azure/defender-for-cloud/plan-defender-for-servers:https://learn.microsoft.com/en-us/rest/api/defenderforcloud/pricings/list:https://learn.microsoft.com/en-us/rest/api/defenderforcloud/pricings/update:https://learn.microsoft.com/en-us/powershell/module/az.security/get-azsecuritypricing:https://learn.microsoft.com/en-us/powershell/module/az.security/set-azsecuritypricing:https://learn.microsoft.com/en-us/security/benchmark/azure/mcsb-endpoint-security#es-1-use-endpoint-detection-and-response-edr", + "DefaultValue": "By default, the Defender for Servers plan is disabled." + } + ] + }, + { + "Id": "8.1.3.2", + "Description": "Ensure that 'Vulnerability assessment for machines' component status is set to 'On'", + "Checks": [ + "defender_auto_provisioning_vulnerabilty_assessments_machines_on" + ], + "Attributes": [ + { + "Section": "8 Security Services", + "SubSection": "8.1 Microsoft Defender for Cloud", + "Profile": "Level 1", + "AssessmentStatus": "Manual", + "Description": "Enable vulnerability assessment for machines on both Azure and hybrid (Arc enabled) machines.", + "RationaleStatement": "Vulnerability assessment for machines scans for various security-related configurations and events such as system updates, OS vulnerabilities, and endpoint protection, then produces alerts on threat and vulnerability findings.", + "ImpactStatement": "Microsoft Defender for Servers plan 2 licensing is required, and configuration of Azure Arc introduces complexity beyond this recommendation.", + "RemediationProcedure": "**Remediate from Azure Portal** 1. From Azure Home select the Portal Menu 1. Select `Microsoft Defender for Cloud` 1. Under `Management`, select `Environment Settings` 1. Select a subscription 1. Click on `Settings & Monitoring` 1. Set the `Status` of `Vulnerability assessment for machines` to `On` 1. Click `Continue` Repeat the above for any additional subscriptions.", + "AuditProcedure": "**Audit from Azure Portal** 1. From Azure Home select the Portal Menu 1. Select `Microsoft Defender for Cloud` 1. Under `Management`, select `Environment Settings` 1. Select a subscription 1. Click on `Settings & monitoring` 1. Ensure that `Vulnerability assessment for machines` is set to `On` Repeat the above for any additional subscriptions.", + "AdditionalInformation": "While this feature is generally available as of publication, it is not yet available for Azure Government tenants.", + "References": "https://docs.microsoft.com/en-us/azure/defender-for-cloud/enable-data-collection?tabs=autoprovision-va:https://msdn.microsoft.com/en-us/library/mt704062.aspx:https://msdn.microsoft.com/en-us/library/mt704063.aspx:https://docs.microsoft.com/en-us/rest/api/securitycenter/autoprovisioningsettings/list:https://docs.microsoft.com/en-us/rest/api/securitycenter/autoprovisioningsettings/create:https://learn.microsoft.com/en-us/security/benchmark/azure/mcsb-posture-vulnerability-management#pv-5-perform-vulnerability-assessments", + "DefaultValue": "By default, `Automatic provisioning of monitoring agent` is set to `Off`." + } + ] + }, + { + "Id": "8.1.3.3", + "Description": "Ensure that 'Endpoint protection' component status is set to 'On'", + "Checks": [ + "defender_assessments_vm_endpoint_protection_installed" + ], + "Attributes": [ + { + "Section": "8 Security Services", + "SubSection": "8.1 Microsoft Defender for Cloud", + "Profile": "Level 1", + "AssessmentStatus": "Automated", + "Description": "The Endpoint protection component enables Microsoft Defender for Endpoint (formerly 'Advanced Threat Protection' or 'ATP' or 'WDATP' - see additional info) to communicate with Microsoft Defender for Cloud. **IMPORTANT:** When enabling integration between DfE & DfC it needs to be taken into account that this will have some side effects that may be undesirable. 1. For server 2019 & above if defender is installed (default for these server SKUs) this will trigger a deployment of the new unified agent and link to any of the extended configuration in the Defender portal. 1. If the new unified agent is required for server SKUs of Win 2016 or Linux and lower there is additional integration that needs to be switched on and agents need to be aligned.", + "RationaleStatement": "Microsoft Defender for Endpoint integration brings comprehensive Endpoint Detection and Response (EDR) capabilities within Microsoft Defender for Cloud. This integration helps to spot abnormalities, as well as detect and respond to advanced attacks on endpoints monitored by Microsoft Defender for Cloud. MDE works only with Standard Tier subscriptions.", + "ImpactStatement": "Endpoint protection requires licensing and is included in these plans: - Defender for Servers plan 1 - Defender for Servers plan 2", + "RemediationProcedure": "**Remediate from Azure Portal** 1. From Azure Home select the Portal Menu. 1. Go to `Microsoft Defender for Cloud`. 1. Under `Management`, select `Environment Settings`. 1. Click on the subscription name. 1. Click `Settings & monitoring`. 1. Set the `Status` for `Endpoint protection` to `On`. 1. Click `Continue`. **Remediate from Azure CLI** Use the below command to enable Standard pricing tier for Storage Accounts ``` az account get-access-token --query {subscription:subscription,accessToken:accessToken} --out tsv | xargs -L1 bash -c 'curl -X PUT -H Authorization: Bearer $1 -H Content-Type: application/json https://management.azure.com/subscriptions//providers/Microsoft.Security/settings/WDATP?api-version=2021-06-01 -d@input.json' ``` Where input.json contains the Request body json data as mentioned below. ``` { id: /subscriptions//providers/Microsoft.Security/settings/WDATP, kind: DataExportSettings, type: Microsoft.Security/settings, properties: { enabled: true } } ```", + "AuditProcedure": "**Audit from Azure Portal** 1. From Azure Home select the Portal Menu. 1. Select `Microsoft Defender for Cloud`. 1. Under `Management`, select `Environment Settings`. 1. Click on the subscription name. 1. Click `Settings & monitoring`. 1. Ensure the `Status` for `Endpoint protection` is set to `On`. **Audit from Azure CLI** Ensure the output of the below command is `True` ``` az account get-access-token --query {subscription:subscription,accessToken:accessToken} --out tsv | xargs -L1 bash -c 'curl -X GET -H Authorization: Bearer $1 -H Content-Type: application/json https://management.azure.com/subscriptions//providers/Microsoft.Security/settings?api-version=2021-06-01' | jq '.|.value[] | select(.name==WDATP)'|jq '.properties.enabled' ``` **Audit from PowerShell** Run the following commands to login and audit this check ``` Connect-AzAccount Set-AzContext -Subscription Get-AzSecuritySetting | Select-Object name,enabled |where-object {$_.name -eq WDATP} ``` **PowerShell Output - Non-Compliant** ``` Name Enabled ---- ------- WDATP False ``` **PowerShell Output - Compliant** ``` Name Enabled ---- ------- WDATP True ```", + "AdditionalInformation": "**IMPORTANT:** When enabling integration between DfE & DfC it needs to be taken into account that this will have some side effects that may be undesirable. 1. For server 2019 & above if defender is installed (default for these server SKUs) this will trigger a deployment of the new unified agent and link to any of the extended configuration in the Defender portal. 1. If the new unified agent is required for server SKUs of Win 2016 or Linux and lower there is additional integration that needs to be switched on and agents need to be aligned. NOTE: Microsoft Defender for Endpoint (MDE) was formerly known as Windows Defender Advanced Threat Protection (WDATP). There are a number of places (e.g. Azure CLI) where the WDATP acronym is still used within Azure.", + "References": "https://docs.microsoft.com/en-in/azure/defender-for-cloud/integration-defender-for-endpoint?tabs=windows:https://docs.microsoft.com/en-us/rest/api/securitycenter/settings/list:https://docs.microsoft.com/en-us/rest/api/securitycenter/settings/update:https://learn.microsoft.com/en-us/security/benchmark/azure/mcsb-endpoint-security#es-1-use-endpoint-detection-and-response-edr:https://learn.microsoft.com/en-us/security/benchmark/azure/mcsb-endpoint-security#es-2-use-modern-anti-malware-software", + "DefaultValue": "By default, Endpoint protection is `off`." + } + ] + }, + { + "Id": "8.1.3.4", + "Description": "Ensure that 'Agentless scanning for machines' component status is set to 'On'", + "Checks": [], + "Attributes": [ + { + "Section": "8 Security Services", + "SubSection": "8.1 Microsoft Defender for Cloud", + "Profile": "Level 1", + "AssessmentStatus": "Manual", + "Description": "Using disk snapshots, the agentless scanner scans for installed software, vulnerabilities, and plain text secrets.", + "RationaleStatement": "The Microsoft Defender for Cloud agentless machine scanner provides threat detection, vulnerability detection, and discovery of sensitive information.", + "ImpactStatement": "Agentless scanning for machines requires licensing and is included in these plans: - Defender CSPM - Defender for Servers plan 2", + "RemediationProcedure": "**Audit from Azure Portal** 1. From the Azure Portal `Home` page, select `Microsoft Defender for Cloud` 1. Under `Management` select `Environment Settings` 1. Select a subscription 1. Under `Settings` > `Defender Plans`, click `Settings & monitoring` 1. Under the Component column, locate the row for `Agentless scanning for machines` 1. Select `On` 1. Click `Continue` in the top left Repeat the above for any additional subscriptions.", + "AuditProcedure": "**Audit from Azure Portal** 1. From the Azure Portal `Home` page, select `Microsoft Defender for Cloud` 1. Under `Management` select `Environment Settings` 1. Select a subscription 1. Under `Settings` > `Defender Plans`, click `Settings & monitoring` 1. Under the Component column, locate the row for `Agentless scanning for machines` 1. Ensure that `On` is selected Repeat the above for any additional subscriptions.", + "AdditionalInformation": "", + "References": "https://learn.microsoft.com/en-us/azure/defender-for-cloud/concept-agentless-data-collection:https://learn.microsoft.com/en-us/security/benchmark/azure/mcsb-incident-response#ir-2-preparation---setup-incident-notification:https://learn.microsoft.com/en-us/azure/defender-for-cloud/enable-agentless-scanning-vms", + "DefaultValue": "By default, Agentless scanning for machines is `off`." + } + ] + }, + { + "Id": "8.1.3.5", + "Description": "Ensure that 'File Integrity Monitoring' component status is set to 'On'", + "Checks": [], + "Attributes": [ + { + "Section": "8 Security Services", + "SubSection": "8.1 Microsoft Defender for Cloud", + "Profile": "Level 1", + "AssessmentStatus": "Manual", + "Description": "File Integrity Monitoring (FIM) is a feature that monitors critical system files in Windows or Linux for potential signs of attack or compromise.", + "RationaleStatement": "FIM provides a detection mechanism for compromised files. When FIM is enabled, critical system files are monitored for changes that might indicate a threat actor is attempting to modify system files for lateral compromise within a host operating system.", + "ImpactStatement": "File Integrity Monitoring requires licensing and is included in these plans: - Defender for Servers plan 2", + "RemediationProcedure": "**Audit from Azure Portal** 1. From the Azure Portal `Home` page, select `Microsoft Defender for Cloud` 1. Under `Management` select `Environment Settings` 1. Select a subscription 1. Under `Settings` > `Defender Plans`, click `Settings & monitoring` 1. Under the Component column, locate the row for `File Integrity Monitoring` 1. Select `On` 1. Click `Continue` in the top left Repeat the above for any additional subscriptions.", + "AuditProcedure": "**Audit from Azure Portal** 1. From the Azure Portal `Home` page, select `Microsoft Defender for Cloud` 1. Under `Management` select `Environment Settings` 1. Select a subscription 1. Under `Settings` > `Defender Plans`, click `Settings & monitoring` 1. Under the Component column, locate the row for `File Integrity Monitoring` 1. Ensure that `On` is selected Repeat the above for any additional subscriptions.", + "AdditionalInformation": "", + "References": "https://learn.microsoft.com/en-us/azure/defender-for-cloud/file-integrity-monitoring-overview:https://learn.microsoft.com/en-us/security/benchmark/azure/mcsb-incident-response#ir-2-preparation---setup-incident-notification:https://learn.microsoft.com/en-us/azure/defender-for-cloud/file-integrity-monitoring-enable-defender-endpoint", + "DefaultValue": "By default, File Integrity Monitoring is `Off`." + } + ] + }, + { + "Id": "8.1.4.1", + "Description": "Ensure That Microsoft Defender for Containers Is Set To 'On'", + "Checks": [ + "defender_ensure_defender_for_containers_is_on" + ], + "Attributes": [ + { + "Section": "8 Security Services", + "SubSection": "8.1 Microsoft Defender for Cloud", + "Profile": "Level 1", + "AssessmentStatus": "Automated", + "Description": "Microsoft Defender for Containers helps improve, monitor, and maintain the security of containerized assets—including Kubernetes clusters, nodes, workloads, container registries, and images—across multi-cloud and on-premises environments. By default, when enabling the plan through the Azure Portal, Microsoft Defender for Containers automatically configures the following components: - **Agentless scanning for machines** - **Defender sensor** for runtime protection - **Azure Policy** for enforcing security best practices - **K8S API access** for monitoring and threat detection - **Registry access** for vulnerability assessment **Note:** Microsoft Defender for Container Registries ('ContainerRegistry') is deprecated and has been replaced by Microsoft Defender for Containers ('Containers').", + "RationaleStatement": "Enabling Microsoft Defender for Containers enhances defense-in-depth by providing advanced threat detection, vulnerability assessment, and security monitoring for containerized environments, leveraging insights from the Microsoft Security Response Center (MSRC).", + "ImpactStatement": "Microsoft Defender for Containers incurs a charge per vCore. Refer to https://azure.microsoft.com/en-us/pricing/details/defender-for-cloud/ and https://azure.microsoft.com/en-us/pricing/calculator/ to estimate potential costs.", + "RemediationProcedure": "**Remediate from Azure Portal** 1. Go to `Microsoft Defender for Cloud`. 1. Under `Management`, click `Environment settings`. 1. Click the name of a subscription. 1. Under `Settings`, click `Defender plans`. 1. Under `Cloud Workload Protection (CWP)`, in the row for `Containers`, click `On` in the `Status` column. 1. If `Monitoring coverage` displays `Partial`, click `Settings` under `Partial`. 1. Set the status of each of the components to `On`. 1. Click `Continue`. 1. Click `Save`. 1. Repeat steps 1-9 for each subscription. **Remediate from Azure CLI** **Note:** Microsoft Defender for Container Registries ('ContainerRegistry') is deprecated and has been replaced by Microsoft Defender for Containers ('Containers'). Run the below command to enable the Microsoft Defender for Containers plan and its components: ``` az security pricing create -n 'Containers' --tier 'standard' --extensions name=ContainerRegistriesVulnerabilityAssessments isEnabled=True --extensions name=AgentlessDiscoveryForKubernetes isEnabled=True --extensions name=AgentlessVmScanning isEnabled=True --extensions name=ContainerSensor isEnabled=True ``` **Remediate from PowerShell** **Note:** Microsoft Defender for Container Registries ('ContainerRegistry') is deprecated and has been replaced by Microsoft Defender for Containers ('Containers'). Run the below command to enable the Microsoft Defender for Containers plan and its components: ``` Set-AzSecurityPricing -Name 'Containers' -PricingTier 'Standard' -Extension '[{name:ContainerRegistriesVulnerabilityAssessments,isEnabled:True},{name:AgentlessDiscoveryForKubernetes,isEnabled:True},{name:AgentlessVmScanning,isEnabled:True},{name:ContainerSensor,isEnabled:True}]' ```", + "AuditProcedure": "**Audit from Azure Portal** 1. Go to `Microsoft Defender for Cloud`. 1. Under `Management`, click `Environment settings`. 1. Click the name of a subscription. 1. Under `Settings`, click `Defender plans`. 1. Under `Cloud Workload Protection (CWP)`, in the row for `Containers`, ensure that the `Status` is set to `On` and `Monitoring coverage` displays `Full`. 1. Repeat steps 1-5 for each subscription. **Audit from Azure CLI** For Microsoft Defender for Container Registries (deprecated), run the following command: ``` az security pricing show --name ContainerRegistry --query pricingTier ``` Ensure that the command returns `Standard`. For Microsoft Defender for Containers, run the following command: ``` az security pricing show --name Containers --query [pricingTier,extensions[*].[name,isEnabled]] ``` Ensure that the command returns `Standard`, and that each of the extensions (ContainerRegistriesVulnerabilityAssessments, AgentlessDiscoveryForKubernetes, AgentlessVmScanning, ContainerSensor) returns `True`. Repeat for each subscription. **Audit from PowerShell** For Microsoft Defender for Container Registries (deprecated), run the following command: ``` Get-AzSecurityPricing -Name 'ContainerRegistry' | Select-Object Name,PricingTier ``` Ensure the command returns `PricingTier` `Standard`. For Microsoft Defender for Containers, run the following command: ``` Get-AzSecurityPricing -Name 'Containers' ``` Ensure that `PricingTier` is set to `Standard`, and that each of the extensions (ContainerRegistriesVulnerabilityAssessments, AgentlessDiscoveryForKubernetes, AgentlessVmScanning, ContainerSensor) has `isEnabled` set to `True`. Repeat for each 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:** [1c988dd6-ade4-430f-a608-2a3e5b0a6d38](https://portal.azure.com/#view/Microsoft_Azure_Policy/PolicyDetailBlade/definitionId/%2Fproviders%2FMicrosoft.Authorization%2FpolicyDefinitions%2F1c988dd6-ade4-430f-a608-2a3e5b0a6d38) **- Name:** 'Microsoft Defender for Containers should be enabled'", + "AdditionalInformation": "The Azure Policy 'Microsoft Defender for Containers should be enabled' checks only that the `pricingTier` for `Containers` is set to `Standard`. It does not check the status of the plan's components.", + "References": "https://learn.microsoft.com/en-us/cli/azure/security/pricing:https://learn.microsoft.com/en-us/powershell/module/az.security/get-azsecuritypricing:https://learn.microsoft.com/en-us/powershell/module/az.security/set-azsecuritypricing:https://learn.microsoft.com/en-us/azure/defender-for-cloud/defender-for-containers-introduction:https://learn.microsoft.com/en-us/azure/defender-for-cloud/tutorial-enable-containers-azure:https://learn.microsoft.com/en-us/security/benchmark/azure/mcsb-logging-threat-detection#lt-1-enable-threat-detection-capabilities", + "DefaultValue": "The Microsoft Defender for Containers plan is disabled by default." + } + ] + }, + { + "Id": "8.1.5.1", + "Description": "Ensure That Microsoft Defender for Storage Is Set To 'On'", + "Checks": [ + "defender_ensure_defender_for_storage_is_on" + ], + "Attributes": [ + { + "Section": "8 Security Services", + "SubSection": "8.1 Microsoft Defender for Cloud", + "Profile": "Level 1", + "AssessmentStatus": "Automated", + "Description": "Turning on Microsoft Defender for Storage enables threat detection for Storage, providing threat intelligence, anomaly detection, and behavior analytics in the Microsoft Defender for Cloud.", + "RationaleStatement": "Enabling Microsoft Defender for Storage allows for greater defense-in-depth, with threat detection provided by the Microsoft Security Response Center (MSRC).", + "ImpactStatement": "Turning on Microsoft Defender for Storage incurs an additional cost per resource.", + "RemediationProcedure": "**Remediate from Azure Portal** 1. Go to `Microsoft Defender for Cloud`. 2. Under `Management`, select `Environment Settings`. 3. Click on the subscription name. 4. Select the `Defender plans` blade. 5. Set `Status` to `On` for `Storage`. 6. Select `Save`. **Remediate from Azure CLI** Ensure the output of the below command is Standard ``` az security pricing create -n StorageAccounts --tier 'standard' ``` **Remediate from PowerShell** ``` Set-AzSecurityPricing -Name 'StorageAccounts' -PricingTier 'Standard' ```", + "AuditProcedure": "**Audit from Azure Portal** 1. Go to `Microsoft Defender for Cloud`. 2. Under `Management`, select `Environment Settings`. 3. Click on the subscription name. 4. Select the `Defender plans` blade. 5. Ensure `Status` is set to `On` for `Storage`. **Audit from Azure CLI** Ensure the output of the below command is Standard ``` az security pricing show -n StorageAccounts ``` **Audit from PowerShell** ``` Get-AzSecurityPricing -Name 'StorageAccounts' | Select-Object Name,PricingTier ``` Ensure output for `Name PricingTier` is `StorageAccounts Standard` **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:** [640d2586-54d2-465f-877f-9ffc1d2109f4](https://portal.azure.com/#view/Microsoft_Azure_Policy/PolicyDetailBlade/definitionId/%2Fproviders%2FMicrosoft.Authorization%2FpolicyDefinitions%2F640d2586-54d2-465f-877f-9ffc1d2109f4) **- Name:** 'Microsoft Defender for Storage should be enabled'", + "AdditionalInformation": "", + "References": "https://docs.microsoft.com/en-us/azure/security-center/security-center-detection-capabilities:https://docs.microsoft.com/en-us/rest/api/securitycenter/pricings/list:https://docs.microsoft.com/en-us/rest/api/securitycenter/pricings/update:https://docs.microsoft.com/en-us/powershell/module/az.security/get-azsecuritypricing:https://learn.microsoft.com/en-us/security/benchmark/azure/mcsb-logging-threat-detection#lt-1-enable-threat-detection-capabilities", + "DefaultValue": "By default, Microsoft Defender plan is off." + } + ] + }, + { + "Id": "8.1.5.2", + "Description": "Ensure Advanced Threat Protection Alerts for Storage Accounts Are Monitored", + "Checks": [], + "Attributes": [ + { + "Section": "8 Security Services", + "SubSection": "8.1 Microsoft Defender for Cloud", + "Profile": "Level 2", + "AssessmentStatus": "Manual", + "Description": "After enabling Microsoft Defender for Storage, configure an alert monitoring and response process to ensure that alerts are actioned in a timely manner. Integrate with SIEM solutions like Microsoft Sentinel, or configure email/webhook notifications to security teams.", + "RationaleStatement": "Enabling Microsoft Defender for Storage without a monitoring process limits its value. Continuous monitoring and alert triage ensure that detected threats are acted upon quickly, reducing risk exposure.", + "ImpactStatement": "Requires integration effort with SIEM or alerting tools and a defined incident response process. The amount of data logged and, thus, the cost incurred can vary significantly depending on the tenant size and the applications in your tenant that interact with the Microsoft Graph APIs. See pricing: Log Analytics (https://learn.microsoft.com/en-us/azure/azure-monitor/logs/cost-logs#pricing-model), Azure Storage (https://azure.microsoft.com/en-us/pricing/details/storage/blobs/), Event Hubs (https://azure.microsoft.com/en-us/pricing/details/event-hubs/).", + "RemediationProcedure": "Connect Microsoft Defender for Cloud to a SIEM such as Microsoft Sentinel or another log analytics solution. **Remediate from Azure Portal** 1. Go to `Microsoft Defender for Cloud`. 2. Under `Management`, click `Environment Settings`. 3. Expand the Tenant Root Group(s) to reveal subscriptions. For each subscription listed: 1. Click the subscription name to open the Defender Plans settings 2. In the settings on the left, click `Continuous Export` 3. Select either `Event Hub`, `Log Analytics Workspace`, or both depending on your environment. 4. Set `Export enabled` to `On` 5. Under `Exported data types`, ensure that at least `Security Alerts (Medium and High)` is checked. 6. Under `Export target`, set the target Event Hub or Log Analytics Workspace which is tied to a SIEM that is configured to monitor and alert for security alerts. Ensure security alerts are included in the security operations workflow and incident response plan.", + "AuditProcedure": "**Audit from Azure Portal** 1. Go to `Microsoft Defender for Cloud`. 2. Under `Management`, click `Environment Settings`. 3. Expand the Tenant Root Group(s) to reveal subscriptions. For each subscription listed: 1. Click the subscription name to open the Defender Plans settings 2. In the settings on the left, click `Continuous Export` Ensure that `Export enabled` is set to `On` and delivering at least `Security Alerts (Medium and High)` to an Event Hub or Log Analytics Workspace which is tied to a SIEM that is configured to monitor and alert for security alerts.", + "AdditionalInformation": "", + "References": "https://learn.microsoft.com/azure/defender-for-cloud/alerts-overview:https://learn.microsoft.com/azure/sentinel/connect-defender-for-cloud:https://learn.microsoft.com/en-us/azure/defender-for-cloud/continuous-export", + "DefaultValue": "By default, continuous export is off." + } + ] + }, + { + "Id": "8.1.6.1", + "Description": "Ensure That Microsoft Defender for App Services Is Set To 'On'", + "Checks": [ + "defender_ensure_defender_for_app_services_is_on" + ], + "Attributes": [ + { + "Section": "8 Security Services", + "SubSection": "8.1 Microsoft Defender for Cloud", + "Profile": "Level 1", + "AssessmentStatus": "Automated", + "Description": "Turning on Microsoft Defender for App Service enables threat detection for App Service, providing threat intelligence, anomaly detection, and behavior analytics in the Microsoft Defender for Cloud.", + "RationaleStatement": "Enabling Microsoft Defender for App Service allows for greater defense-in-depth, with threat detection provided by the Microsoft Security Response Center (MSRC).", + "ImpactStatement": "Turning on Microsoft Defender for App Service incurs an additional cost per resource.", + "RemediationProcedure": "**Remediate from Azure Portal** 1. Go to `Microsoft Defender for Cloud` 2. Under `Management`, select `Environment Settings` 3. Click on the subscription name 4. Select `Defender plans` 5. Set `App Service` Status to `On` 6. Select `Save` **Remediate from Azure CLI** Run the following command: ``` az security pricing create -n Appservices --tier 'standard' ``` **Remediate from PowerShell** Run the following command: ``` Set-AzSecurityPricing -Name AppServices -PricingTier Standard ```", + "AuditProcedure": "**Audit from Azure Portal** 1. Go to `Microsoft Defender for Cloud` 2. Under `Management`, select `Environment Settings` 3. Click on the subscription name 4. Select `Defender plans` 5. Ensure Status is `On` for `App Service` **Audit from Azure CLI** Run the following command: ``` az security pricing show -n AppServices ``` Ensure `-PricingTier` is set to `Standard` **Audit from PowerShell** Run the following command: ``` Get-AzSecurityPricing -Name 'AppServices' |Select-Object Name,PricingTier ``` Ensure the `-PricingTier` is set to `Standard` **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:** [2913021d-f2fd-4f3d-b958-22354e2bdbcb](https://portal.azure.com/#view/Microsoft_Azure_Policy/PolicyDetailBlade/definitionId/%2Fproviders%2FMicrosoft.Authorization%2FpolicyDefinitions%2F2913021d-f2fd-4f3d-b958-22354e2bdbcb) **- Name:** 'Azure Defender for App Service should be enabled'", + "AdditionalInformation": "", + "References": "https://docs.microsoft.com/en-us/azure/security-center/security-center-detection-capabilities:https://docs.microsoft.com/en-us/rest/api/securitycenter/pricings/list:https://docs.microsoft.com/en-us/rest/api/securitycenter/pricings/update:https://docs.microsoft.com/en-us/powershell/module/az.security/get-azsecuritypricing:https://learn.microsoft.com/en-us/security/benchmark/azure/mcsb-logging-threat-detection#lt-1-enable-threat-detection-capabilities", + "DefaultValue": "By default, Microsoft Defender plan is off." + } + ] + }, + { + "Id": "8.1.7.1", + "Description": "Ensure That Microsoft Defender for Azure Cosmos DB Is Set To 'On'", + "Checks": [ + "defender_ensure_defender_for_cosmosdb_is_on" + ], + "Attributes": [ + { + "Section": "8 Security Services", + "SubSection": "8.1 Microsoft Defender for Cloud", + "Profile": "Level 1", + "AssessmentStatus": "Automated", + "Description": "Microsoft Defender for Azure Cosmos DB scans all incoming network requests for threats to your Azure Cosmos DB resources.", + "RationaleStatement": "In scanning Azure Cosmos DB requests within a subscription, requests are compared to a heuristic list of potential security threats. These threats could be a result of a security breach within your services, thus scanning for them could prevent a potential security threat from being introduced.", + "ImpactStatement": "Enabling Microsoft Defender for Azure Cosmos DB requires enabling Microsoft Defender for your subscription. Both will incur additional charges.", + "RemediationProcedure": "**Remediate from Azure Portal** 1. Go to `Microsoft Defender for Cloud`. 2. Under `Management`, select `Environment Settings`. 3. Click on the subscription name. 4. Select the `Defender plans` blade. 5. On the `Database` row click on `Select types >`. 6. Set the toggle switch next to `Azure Cosmos DB` to `On`. 7. Click `Continue`. 8. Click `Save`. **Remediate from Azure CLI** Run the following command: ``` az security pricing create -n 'CosmosDbs' --tier 'standard' ``` **Remediate from PowerShell** Use the below command to enable Standard pricing tier for Azure Cosmos DB ``` Set-AzSecurityPricing -Name 'CosmosDbs' -PricingTier 'Standard ```", + "AuditProcedure": "**Audit from Azure Portal** 1. Go to `Microsoft Defender for Cloud`. 2. Under `Management`, select `Environment Settings`. 3. Click on the subscription name. 4. Select the `Defender plans` blade. 5. On the `Database` row click on `Select types >`. 6. Ensure the toggle switch next to `Azure Cosmos DB` is set to `On`. **Audit from Azure CLI** Ensure the output of the below command is Standard ``` az security pricing show -n CosmosDbs --query pricingTier ``` **Audit from PowerShell** ``` Get-AzSecurityPricing -Name 'CosmosDbs' | Select-Object Name,PricingTier ``` Ensure output of `-PricingTier` is `Standard` **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:** [adbe85b5-83e6-4350-ab58-bf3a4f736e5e](https://portal.azure.com/#view/Microsoft_Azure_Policy/PolicyDetailBlade/definitionId/%2Fproviders%2FMicrosoft.Authorization%2FpolicyDefinitions%2Fadbe85b5-83e6-4350-ab58-bf3a4f736e5e) **- Name:** 'Microsoft Defender for Azure Cosmos DB should be enabled'", + "AdditionalInformation": "", + "References": "https://azure.microsoft.com/en-us/pricing/details/defender-for-cloud/:https://docs.microsoft.com/en-us/azure/defender-for-cloud/enable-enhanced-security:https://docs.microsoft.com/en-us/azure/defender-for-cloud/alerts-overview:https://docs.microsoft.com/en-us/security/benchmark/azure/baselines/cosmos-db-security-baseline:https://docs.microsoft.com/en-us/azure/defender-for-cloud/quickstart-enable-database-protections:https://learn.microsoft.com/en-us/security/benchmark/azure/mcsb-logging-threat-detection#lt-1-enable-threat-detection-capabilities", + "DefaultValue": "By default, Microsoft Defender for Azure Cosmos DB is not enabled." + } + ] + }, + { + "Id": "8.1.7.2", + "Description": "Ensure That Microsoft Defender for Open-Source Relational Databases Is Set To 'On'", + "Checks": [ + "defender_ensure_defender_for_os_relational_databases_is_on" + ], + "Attributes": [ + { + "Section": "8 Security Services", + "SubSection": "8.1 Microsoft Defender for Cloud", + "Profile": "Level 1", + "AssessmentStatus": "Automated", + "Description": "Turning on Microsoft Defender for Open-source relational databases enables threat detection for Open-source relational databases, providing threat intelligence, anomaly detection, and behavior analytics in the Microsoft Defender for Cloud.", + "RationaleStatement": "Enabling Microsoft Defender for Open-source relational databases allows for greater defense-in-depth, with threat detection provided by the Microsoft Security Response Center (MSRC).", + "ImpactStatement": "Turning on Microsoft Defender for Open-source relational databases incurs an additional cost per resource.", + "RemediationProcedure": "**Remediate from Azure Portal** 1. Go to `Microsoft Defender for Cloud`. 2. Under `Management`, select `Environment Settings`. 3. Click on the subscription name. 4. Select the `Defender plans` blade. 5. Click `Select types >` in the row for `Databases`. 6. Set the toggle switch next to `Open-source relational databases` to `On`. 7. Select `Continue`. 8. Select `Save`. **Remediate from Azure CLI** Run the following command: ``` az security pricing create -n 'OpenSourceRelationalDatabases' --tier 'standard' ``` **Remediate from PowerShell** Use the below command to enable Standard pricing tier for Open-source relational databases ``` set-azsecuritypricing -name OpenSourceRelationalDatabases -pricingtier Standard ```", + "AuditProcedure": "**Audit from Azure Portal** 1. Go to `Microsoft Defender for Cloud`. 1. Under `Management`, select `Environment Settings`. 1. Click on the subscription name. 1. Select the `Defender plans` blade. 1. Click `Select types >` in the row for `Databases`. 1. Ensure the toggle switch next to `Open-source relational databases` is set to `On`. **Audit from Azure CLI** Run the following command: ``` az security pricing show -n OpenSourceRelationalDatabases --query pricingTier ``` **Audit from PowerShell** ``` Get-AzSecurityPricing | Where-Object {$_.Name -eq 'OpenSourceRelationalDatabases'} | Select-Object Name, PricingTier ``` Ensure output for `Name PricingTier` is `OpenSourceRelationalDatabases Standard` **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:** [0a9fbe0d-c5c4-4da8-87d8-f4fd77338835](https://portal.azure.com/#view/Microsoft_Azure_Policy/PolicyDetailBlade/definitionId/%2Fproviders%2FMicrosoft.Authorization%2FpolicyDefinitions%2F0a9fbe0d-c5c4-4da8-87d8-f4fd77338835) **- Name:** 'Azure Defender for open-source relational databases should be enabled'", + "AdditionalInformation": "", + "References": "https://docs.microsoft.com/en-us/azure/security-center/security-center-detection-capabilities:https://docs.microsoft.com/en-us/rest/api/securitycenter/pricings/update:https://docs.microsoft.com/en-us/powershell/module/az.security/get-azsecuritypricing:https://learn.microsoft.com/en-us/security/benchmark/azure/mcsb-data-protection#dp-2-monitor-anomalies-and-threats-targeting-sensitive-data:https://learn.microsoft.com/en-us/security/benchmark/azure/mcsb-logging-threat-detection#lt-1-enable-threat-detection-capabilities", + "DefaultValue": "By default, Microsoft Defender plan is off." + } + ] + }, + { + "Id": "8.1.7.3", + "Description": "Ensure That Microsoft Defender for (Managed Instance) Azure SQL Databases Is Set To 'On'", + "Checks": [ + "defender_ensure_defender_for_azure_sql_databases_is_on" + ], + "Attributes": [ + { + "Section": "8 Security Services", + "SubSection": "8.1 Microsoft Defender for Cloud", + "Profile": "Level 1", + "AssessmentStatus": "Automated", + "Description": "Turning on Microsoft Defender for Azure SQL Databases enables threat detection for Managed Instance Azure SQL databases, providing threat intelligence, anomaly detection, and behavior analytics in Microsoft Defender for Cloud.", + "RationaleStatement": "Enabling Microsoft Defender for Azure SQL Databases allows for greater defense-in-depth, includes functionality for discovering and classifying sensitive data, surfacing and mitigating potential database vulnerabilities, and detecting anomalous activities that could indicate a threat to your database.", + "ImpactStatement": "Turning on Microsoft Defender for Azure SQL Databases incurs an additional cost per resource.", + "RemediationProcedure": "**Remediate from Azure Portal** 1. Go to `Microsoft Defender for Cloud`. 2. Under `Management`, select `Environment Settings`. 3. Click on the subscription name. 4. Select the `Defender plans` blade. 5. Click `Select types >` in the row for `Databases`. 6. Set the toggle switch next to `Azure SQL Databases` to `On`. 7. Select `Continue`. 8. Select `Save`. **Remediate from Azure CLI** Run the following command: ``` az security pricing create -n SqlServers --tier 'standard' ``` **Remediate from PowerShell** Run the following command: ``` Set-AzSecurityPricing -Name 'SqlServers' -PricingTier 'Standard' ```", + "AuditProcedure": "**Audit from Azure Portal** 1. Go to `Microsoft Defender for Cloud`. 2. Under `Management`, select `Environment Settings`. 3. Click on the subscription name. 4. Select the `Defender plans` blade. 5. Click `Select types >` in the row for `Databases`. 6. Ensure the toggle switch next to `Azure SQL Databases` is set to `On`. **Audit from Azure CLI** Run the following command: ``` az security pricing show -n SqlServers ``` Ensure `-PricingTier` is set to `Standard` **Audit from PowerShell** Run the following command: ``` Get-AzSecurityPricing -Name 'SqlServers' | Select-Object Name,PricingTier ``` Ensure the `-PricingTier` is set to `Standard` **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:** [7fe3b40f-802b-4cdd-8bd4-fd799c948cc2](https://portal.azure.com/#view/Microsoft_Azure_Policy/PolicyDetailBlade/definitionId/%2Fproviders%2FMicrosoft.Authorization%2FpolicyDefinitions%2F7fe3b40f-802b-4cdd-8bd4-fd799c948cc2) **- Name:** 'Azure Defender for Azure SQL Database servers should be enabled' - **Policy ID:** [abfb7388-5bf4-4ad7-ba99-2cd2f41cebb9](https://portal.azure.com/#view/Microsoft_Azure_Policy/PolicyDetailBlade/definitionId/%2Fproviders%2FMicrosoft.Authorization%2FpolicyDefinitions%2Fabfb7388-5bf4-4ad7-ba99-2cd2f41cebb9) **- Name:** 'Azure Defender for SQL should be enabled for unprotected SQL Managed Instances'", + "AdditionalInformation": "", + "References": "https://docs.microsoft.com/en-us/azure/security-center/security-center-detection-capabilities:https://docs.microsoft.com/en-us/rest/api/securitycenter/pricings/list:https://docs.microsoft.com/en-us/rest/api/securitycenter/pricings/update:https://docs.microsoft.com/en-us/powershell/module/az.security/get-azsecuritypricing:https://learn.microsoft.com/en-us/security/benchmark/azure/mcsb-data-protection#dp-2-monitor-anomalies-and-threats-targeting-sensitive-data:https://learn.microsoft.com/en-us/security/benchmark/azure/mcsb-logging-threat-detection#lt-1-enable-threat-detection-capabilities", + "DefaultValue": "By default, Microsoft Defender plan is off." + } + ] + }, + { + "Id": "8.1.7.4", + "Description": "Ensure That Microsoft Defender for SQL Servers on Machines Is Set To 'On'", + "Checks": [ + "defender_ensure_defender_for_sql_servers_is_on" + ], + "Attributes": [ + { + "Section": "8 Security Services", + "SubSection": "8.1 Microsoft Defender for Cloud", + "Profile": "Level 1", + "AssessmentStatus": "Automated", + "Description": "Turning on Microsoft Defender for SQL servers on machines enables threat detection for SQL servers on machines, providing threat intelligence, anomaly detection, and behavior analytics in Microsoft Defender for Cloud.", + "RationaleStatement": "Enabling Microsoft Defender for SQL servers on machines allows for greater defense-in-depth, functionality for discovering and classifying sensitive data, surfacing and mitigating potential database vulnerabilities, and detecting anomalous activities that could indicate a threat to your database.", + "ImpactStatement": "Turning on Microsoft Defender for SQL servers on machines incurs an additional cost per resource.", + "RemediationProcedure": "**Remediate from Azure Portal** 1. Go to `Microsoft Defender for Cloud`. 2. Under `Management`, select `Environment Settings`. 3. Click on the subscription name. 4. Select the `Defender plans` blade. 5. Click `Select types >` in the row for `Databases`. 6. Set the toggle switch next to `SQL servers on machines` to `On`. 7. Select `Continue`. 8. Select `Save`. **Remediate from Azure CLI** Run the following command: ``` az security pricing create -n SqlServerVirtualMachines --tier 'standard' ``` **Remediate from PowerShell** Run the following command: ``` Set-AzSecurityPricing -Name 'SqlServerVirtualMachines' -PricingTier 'Standard' ```", + "AuditProcedure": "**Audit from Azure Portal** 1. Go to `Microsoft Defender for Cloud`. 2. Under `Management`, select `Environment Settings`. 3. Click on the subscription name. 4. Select the `Defender plans` blade. 5. Click `Select types >` in the row for `Databases`. 6. Ensure the toggle switch next to `SQL servers on machines` is set to `On`. **Audit from Azure CLI** Ensure Defender for SQL is licensed with the following command: ``` az security pricing show -n SqlServerVirtualMachines ``` Ensure the 'PricingTier' is set to 'Standard' **Audit from PowerShell** Run the following command: ``` Get-AzSecurityPricing -Name 'SqlServerVirtualMachines' | Select-Object Name,PricingTier ``` Ensure the 'PricingTier' is set to 'Standard' **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:** [6581d072-105e-4418-827f-bd446d56421b](https://portal.azure.com/#view/Microsoft_Azure_Policy/PolicyDetailBlade/definitionId/%2Fproviders%2FMicrosoft.Authorization%2FpolicyDefinitions%2F6581d072-105e-4418-827f-bd446d56421b) **- Name:** 'Azure Defender for SQL servers on machines should be enabled' - **Policy ID:** [abfb4388-5bf4-4ad7-ba82-2cd2f41ceae9](https://portal.azure.com/#view/Microsoft_Azure_Policy/PolicyDetailBlade/definitionId/%2Fproviders%2FMicrosoft.Authorization%2FpolicyDefinitions%2Fabfb4388-5bf4-4ad7-ba82-2cd2f41ceae9) **- Name:** 'Azure Defender for SQL should be enabled for unprotected Azure SQL servers'", + "AdditionalInformation": "", + "References": "https://docs.microsoft.com/en-us/azure/security-center/defender-for-sql-usage:https://docs.microsoft.com/en-us/azure/security-center/security-center-detection-capabilities:https://docs.microsoft.com/en-us/rest/api/securitycenter/pricings/update:https://docs.microsoft.com/en-us/powershell/module/az.security/get-azsecuritypricing:https://learn.microsoft.com/en-us/security/benchmark/azure/mcsb-data-protection#dp-2-monitor-anomalies-and-threats-targeting-sensitive-data:https://learn.microsoft.com/en-us/security/benchmark/azure/mcsb-logging-threat-detection#lt-1-enable-threat-detection-capabilities", + "DefaultValue": "By default, Microsoft Defender plan is off." + } + ] + }, + { + "Id": "8.1.8.1", + "Description": "Ensure That Microsoft Defender for Key Vault Is Set To 'On'", + "Checks": [ + "defender_ensure_defender_for_keyvault_is_on" + ], + "Attributes": [ + { + "Section": "8 Security Services", + "SubSection": "8.1 Microsoft Defender for Cloud", + "Profile": "Level 1", + "AssessmentStatus": "Automated", + "Description": "Turning on Microsoft Defender for Key Vault enables threat detection for Key Vault, providing threat intelligence, anomaly detection, and behavior analytics in the Microsoft Defender for Cloud.", + "RationaleStatement": "Enabling Microsoft Defender for Key Vault allows for greater defense-in-depth, with threat detection provided by the Microsoft Security Response Center (MSRC).", + "ImpactStatement": "Turning on Microsoft Defender for Key Vault incurs an additional cost per resource.", + "RemediationProcedure": "**Remediate from Azure Portal** 1. Go to `Microsoft Defender for Cloud`. 2. Under `Management`, select `Environment Settings`. 3. Click on the subscription name. 4. Select the `Defender plans` blade. 5. Select `On` under `Status` for `Key Vault`. 6. Select `Save`. **Remediate from Azure CLI** Enable Standard pricing tier for Key Vault: ``` az security pricing create -n 'KeyVaults' --tier 'Standard' ``` **Remediate from PowerShell** Enable Standard pricing tier for Key Vault: ``` Set-AzSecurityPricing -Name 'KeyVaults' -PricingTier 'Standard' ```", + "AuditProcedure": "**Audit from Azure Portal** 1. Go to `Microsoft Defender for Cloud`. 2. Under `Management`, select `Environment Settings`. 3. Click on the subscription name. 4. Select the `Defender plans` blade. 5. Ensure `Status` is set to `On` for `Key Vault`. **Audit from Azure CLI** Ensure the output of the below command is Standard ``` az security pricing show -n 'KeyVaults' --query 'pricingTier' ``` **Audit from PowerShell** ``` Get-AzSecurityPricing -Name 'KeyVaults' | Select-Object Name,PricingTier ``` Ensure output for `PricingTier` is `Standard` **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:** [0e6763cc-5078-4e64-889d-ff4d9a839047](https://portal.azure.com/#view/Microsoft_Azure_Policy/PolicyDetailBlade/definitionId/%2Fproviders%2FMicrosoft.Authorization%2FpolicyDefinitions%2F0e6763cc-5078-4e64-889d-ff4d9a839047) **- Name:** 'Azure Defender for Key Vault should be enabled'", + "AdditionalInformation": "", + "References": "https://docs.microsoft.com/en-us/azure/security-center/security-center-detection-capabilities:https://docs.microsoft.com/en-us/rest/api/securitycenter/pricings/list:https://docs.microsoft.com/en-us/rest/api/securitycenter/pricings/update:https://docs.microsoft.com/en-us/powershell/module/az.security/get-azsecuritypricing:https://learn.microsoft.com/en-us/security/benchmark/azure/mcsb-logging-threat-detection#lt-1-enable-threat-detection-capabilities", + "DefaultValue": "By default, Microsoft Defender plan is off." + } + ] + }, + { + "Id": "8.1.9.1", + "Description": "Ensure That Microsoft Defender for Resource Manager Is Set To 'On'", + "Checks": [ + "defender_ensure_defender_for_arm_is_on" + ], + "Attributes": [ + { + "Section": "8 Security Services", + "SubSection": "8.1 Microsoft Defender for Cloud", + "Profile": "Level 1", + "AssessmentStatus": "Automated", + "Description": "Microsoft Defender for Resource Manager scans incoming administrative requests to change your infrastructure from both CLI and the Azure portal.", + "RationaleStatement": "Scanning resource requests lets you be alerted every time there is suspicious activity in order to prevent a security threat from being introduced.", + "ImpactStatement": "Enabling Microsoft Defender for Resource Manager requires enabling Microsoft Defender for your subscription. Both will incur additional charges.", + "RemediationProcedure": "**Remediate from Azure Portal** 1. Go to `Microsoft Defender for Cloud`. 2. Under `Management`, select `Environment Settings`. 3. Click on the subscription name. 4. Select the `Defender plans` blade. 5. Select `On` under `Status` for `Resource Manager`. 6. Select `Save. **Remediate from Azure CLI** Use the below command to enable Standard pricing tier for Defender for Resource Manager ``` az security pricing create -n 'Arm' --tier 'Standard' ``` **Remediate from PowerShell** Use the below command to enable Standard pricing tier for Defender for Resource Manager ``` Set-AzSecurityPricing -Name 'Arm' -PricingTier 'Standard' ```", + "AuditProcedure": "**Audit from Azure Portal** 1. Go to `Microsoft Defender for Cloud`. 2. Under `Management`, select `Environment Settings`. 3. Click on the subscription name. 4. Select the `Defender plans` blade. 5. Ensure `Status` is set to `On` for `Resource Manager`. **Audit from Azure CLI** Ensure the output of the below command is Standard ``` az security pricing show -n 'Arm' --query 'pricingTier' ``` **Audit from PowerShell** ``` Get-AzSecurityPricing -Name 'Arm' | Select-Object Name,PricingTier ``` Ensure the output of `PricingTier` is `Standard` **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:** [c3d20c29-b36d-48fe-808b-99a87530ad99](https://portal.azure.com/#view/Microsoft_Azure_Policy/PolicyDetailBlade/definitionId/%2Fproviders%2FMicrosoft.Authorization%2FpolicyDefinitions%2Fc3d20c29-b36d-48fe-808b-99a87530ad99) **- Name:** 'Azure Defender for Resource Manager should be enabled'", + "AdditionalInformation": "", + "References": "https://docs.microsoft.com/en-us/azure/defender-for-cloud/enable-enhanced-security:https://docs.microsoft.com/en-us/azure/defender-for-cloud/defender-for-resource-manager-introduction:https://azure.microsoft.com/en-us/pricing/details/defender-for-cloud/:https://docs.microsoft.com/en-us/azure/defender-for-cloud/alerts-overview:https://learn.microsoft.com/en-us/security/benchmark/azure/mcsb-logging-threat-detection#lt-1-enable-threat-detection-capabilities", + "DefaultValue": "By default, Microsoft Defender for Resource Manager is not enabled." + } + ] + }, + { + "Id": "8.1.10", + "Description": "Ensure that Microsoft Defender for Cloud is configured to check VM operating systems for updates", + "Checks": [ + "defender_ensure_system_updates_are_applied" + ], + "Attributes": [ + { + "Section": "8 Security Services", + "SubSection": "8.1 Microsoft Defender for Cloud", + "Profile": "Level 1", + "AssessmentStatus": "Automated", + "Description": "Ensure that the latest OS patches for all virtual machines are applied.", + "RationaleStatement": "Windows and Linux virtual machines should be kept updated to: - Address a specific bug or flaw - Improve an OS or applications general stability - Fix a security vulnerability Microsoft Defender for Cloud retrieves a list of available security and critical updates from Windows Update or Windows Server Update Services (WSUS), depending on which service is configured on a Windows VM. The security center also checks for the latest updates in Linux systems. If a VM is missing a system update, the security center will recommend system updates be applied.", + "ImpactStatement": "Running Microsoft Defender for Cloud incurs additional charges for each resource monitored. Please see attached reference for exact charges per hour.", + "RemediationProcedure": "Follow Microsoft Azure documentation to apply security patches from the security center. Alternatively, you can employ your own patch assessment and management tool to periodically assess, report, and install the required security patches for your OS.", + "AuditProcedure": "**Audit from Azure Portal** 1. From Azure Home select the Portal Menu 1. Select `Microsoft Defender for Cloud` 1. Then the `Recommendations` blade 1. Ensure that there are no recommendations for `System updates should be installed on your machines (powered by Update Center)` Alternatively, you can employ your own patch assessment and management tool to periodically assess, report and install the required security patches for your OS. **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:** [f85bf3e0-d513-442e-89c3-1784ad63382b](https://portal.azure.com/#view/Microsoft_Azure_Policy/PolicyDetailBlade/definitionId/%2Fproviders%2FMicrosoft.Authorization%2FpolicyDefinitions%2Ff85bf3e0-d513-442e-89c3-1784ad63382b) **- Name:** 'System updates should be installed on your machines (powered by Update Center)' - **Policy ID:** [bd876905-5b84-4f73-ab2d-2e7a7c4568d9](https://portal.azure.com/#view/Microsoft_Azure_Policy/PolicyDetailBlade/definitionId/%2Fproviders%2FMicrosoft.Authorization%2FpolicyDefinitions%2Fbd876905-5b84-4f73-ab2d-2e7a7c4568d9) **- Name:** 'Machines should be configured to periodically check for missing system updates'", + "AdditionalInformation": "", + "References": "https://learn.microsoft.com/en-us/security/benchmark/azure/mcsb-posture-vulnerability-management#pv-6-rapidly-and-automatically-remediate-vulnerabilities:https://azure.microsoft.com/en-us/pricing/details/defender-for-cloud/:https://docs.microsoft.com/en-us/azure/defender-for-cloud/deploy-vulnerability-assessment-vm", + "DefaultValue": "By default, patches are not automatically deployed." + } + ] + }, + { + "Id": "8.1.11", + "Description": "Ensure that Microsoft Cloud Security Benchmark policies are not set to 'Disabled'", + "Checks": [ + "policy_ensure_asc_enforcement_enabled" + ], + "Attributes": [ + { + "Section": "8 Security Services", + "SubSection": "8.1 Microsoft Defender for Cloud", + "Profile": "Level 1", + "AssessmentStatus": "Manual", + "Description": "The Microsoft Cloud Security Benchmark (or MCSB) is an Azure Policy Initiative containing many security policies to evaluate resource configuration against best practice recommendations. If a policy in the MCSB is set with effect type `Disabled`, it is not evaluated and may prevent administrators from being informed of valuable security recommendations.", + "RationaleStatement": "A security policy defines the desired configuration of resources in your environment and helps ensure compliance with company or regulatory security requirements. The MCSB Policy Initiative a set of security recommendations based on best practices and is associated with every subscription by default. When a policy Effect is set to `Audit`, policies in the MCSB ensure that Defender for Cloud evaluates relevant resources for supported recommendations. To ensure that policies within the MCSB are not being missed when the Policy Initiative is evaluated, none of the policies should have an Effect of `Disabled`.", + "ImpactStatement": "Policies within the MCSB default to an effect of `Audit` and will evaluate—but not enforce—policy recommendations. Ensuring these policies are set to `Audit` simply ensures that the evaluation occurs to allow administrators to understand where an improvement may be possible. Administrators will need to determine if the recommendations are relevant and desirable for their environment, then manually take action to resolve the status if desired.", + "RemediationProcedure": "**Remediate from Azure Portal** 1. From Azure Home select the Portal Menu. 1. Select `Microsoft Defender for Cloud`. 1. Under `Management`, select `Environment settings`. 1. Click on the appropriate Management Group or Subscription. 1. Click on `Security policies` in the left column. 1. Click on `Microsoft cloud security benchmark` 1. Click `Add Filter` and select `Effect` 1. Check the `Disabled` box to search for all disabled policies 1. Click `Apply` 1. Click the blue ellipsis `...` to the right of a policy name. 1. Click `Manage effect and parameters`. 1. Under `Policy effect`, select the radio button next to `Audit`. 1. Click `Save`. 1. Click `Refresh`. 1. Repeat steps 10-14 until all disabled policies are updated. 1. Repeat steps 1-15 for each Management Group or Subscription requiring remediation.", + "AuditProcedure": "**Audit from Azure Portal** 1. From Azure Home select the Portal Menu. 1. Select `Microsoft Defender for Cloud`. 1. Under `Management`, select `Environment settings`. 1. Click on the appropriate Management Group or Subscription. 1. Click on `Security policies` in the left column. 1. Click on `Microsoft cloud security benchmark`. 1. Click `Add filter` and select `Effect`. 1. Check the `Disabled` box to search for all disabled policies. 1. Click `Apply`. 1. Ensure that no policies are displayed, signifying that there are no disabled policies. 1. Repeat steps 1-10 for each Management Group or Subscription.", + "AdditionalInformation": "", + "References": "https://learn.microsoft.com/en-in/azure/defender-for-cloud/security-policy-concept:https://docs.microsoft.com/en-us/azure/security-center/security-center-policies:https://learn.microsoft.com/en-us/azure/defender-for-cloud/implement-security-recommendations:https://learn.microsoft.com/en-us/rest/api/policy/policy-assignments/get:https://learn.microsoft.com/en-us/rest/api/policy/policy-assignments/create:https://learn.microsoft.com/en-us/security/benchmark/azure/mcsb-governance-strategy#gs-7-define-and-implement-logging-threat-detection-and-incident-response-strategy", + "DefaultValue": "By default, the MCSB policy initiative is assigned on all subscriptions, and **most** policies will have an effect of `Audit`. Some policies will have a default effect of `Disabled`." + } + ] + }, + { + "Id": "8.1.12", + "Description": "Ensure That 'All users with the following roles' is set to 'Owner'", + "Checks": [ + "defender_ensure_notify_emails_to_owners" + ], + "Attributes": [ + { + "Section": "8 Security Services", + "SubSection": "8.1 Microsoft Defender for Cloud", + "Profile": "Level 1", + "AssessmentStatus": "Automated", + "Description": "Enable security alert emails to subscription owners.", + "RationaleStatement": "Enabling security alert emails to subscription owners ensures that they receive security alert emails from Microsoft. This ensures that they are aware of any potential security issues and can mitigate the risk in a timely fashion.", + "ImpactStatement": "Owners will receive email notifications for security alerts.", + "RemediationProcedure": "**Remediate from Azure Portal** 1. From Azure Home select the Portal Menu 1. Select `Microsoft Defender for Cloud` 1. Under `Management`, select `Environment Settings` 1. Click on the appropriate Management Group, Subscription, or Workspace 1. Click on `Email notifications` 1. In the drop down of the `All users with the following roles` field select `Owner` 1. Click `Save` **Remediate from Azure CLI** Use the below command to set `Send email also to subscription owners` to `On`. ``` az account get-access-token --query {subscription:subscription,accessToken:accessToken} --out tsv | xargs -L1 bash -c 'curl -X PUT -H Authorization: Bearer $1 -H Content-Type: application/json https://management.azure.com/subscriptions/$0/providers/Microsoft.Security/securityContacts/default1?api-version=2017-08-01-preview -d@input.json' ``` Where `input.json` contains the data below, replacing `validEmailAddress` with a single email address or multiple comma-separated email addresses: ``` { id: /subscriptions//providers/Microsoft.Security/securityContacts/default1, name: default1, type: Microsoft.Security/securityContacts, properties: { email: , alertNotifications: On, alertsToAdmins: On, notificationsByRole: Owner } } ```", + "AuditProcedure": "**Audit from Azure Portal** 1. From Azure Home select the Portal Menu 1. Select `Microsoft Defender for Cloud` 1. Under `Management`, select `Environment Settings` 1. Click on the appropriate Management Group, Subscription, or Workspace 1. Click on `Email notifications` 1. Ensure that `All users with the following roles` is set to `Owner` **Audit from Azure CLI** Ensure the command below returns state of `On` and that `Owner` appears in roles. ``` az account get-access-token --query {subscription:subscription,accessToken:accessToken} --out tsv | xargs -L1 bash -c 'curl -X GET -H Authorization: Bearer $1 -H Content-Type: application/json https://management.azure.com/subscriptions/$0/providers/Microsoft.Security/securityContacts?api-version=2020-01-01-preview'| jq '.[] | select(.name==default).properties.notificationsByRole' ```", + "AdditionalInformation": "Excluding any entries in the input.json properties block disables the specific setting by default.", + "References": "https://docs.microsoft.com/en-us/azure/security-center/security-center-provide-security-contact-details:https://docs.microsoft.com/en-us/rest/api/securitycenter/securitycontacts/list:https://docs.microsoft.com/en-us/rest/api/securitycenter/security-contacts:https://learn.microsoft.com/en-us/security/benchmark/azure/mcsb-incident-response#ir-2-preparation---setup-incident-notification", + "DefaultValue": "By default, `Owner` is selected" + } + ] + }, + { + "Id": "8.1.13", + "Description": "Ensure 'Additional email addresses' is Configured with a Security Contact Email", + "Checks": [ + "defender_additional_email_configured_with_a_security_contact" + ], + "Attributes": [ + { + "Section": "8 Security Services", + "SubSection": "8.1 Microsoft Defender for Cloud", + "Profile": "Level 1", + "AssessmentStatus": "Automated", + "Description": "Microsoft Defender for Cloud emails the subscription owners whenever a high-severity alert is triggered for their subscription. You should provide a security contact email address as an additional email address.", + "RationaleStatement": "Microsoft Defender for Cloud emails the Subscription Owner to notify them about security alerts. Adding your Security Contact's email address to the 'Additional email addresses' field ensures that your organization's Security Team is included in these alerts. This ensures that the proper people are aware of any potential compromise in order to mitigate the risk in a timely fashion.", + "ImpactStatement": "Security contacts will receive email notifications.", + "RemediationProcedure": "**Remediate from Azure Portal** 1. From Azure Home select the Portal Menu. 1. Select `Microsoft Defender for Cloud`. 1. Under `Management`, select `Environment Settings`. 1. Click on the appropriate Management Group, Subscription, or Workspace. 1. Click on `Email notifications`. 1. Enter a valid security contact email address (or multiple addresses separated by commas) in the `Additional email addresses` field. 1. Click `Save`. **Remediate from Azure CLI** Use the below command to set `Security contact emails` to `On`. ``` az account get-access-token --query {subscription:subscription,accessToken:accessToken} --out tsv | xargs -L1 bash -c 'curl -X PUT -H Authorization: Bearer $1 -H Content-Type: application/json https://management.azure.com/subscriptions/$0/providers/Microsoft.Security/securityContacts/default?api-version=2020-01-01-preview -d@input.json' ``` Where `input.json` contains the data below, replacing `validEmailAddress` with a single email address or multiple comma-separated email addresses: ``` { id: /subscriptions//providers/Microsoft.Security/securityContacts/default, name: default, type: Microsoft.Security/securityContacts, properties: { email: , alertNotifications: On, alertsToAdmins: On } } ```", + "AuditProcedure": "**Audit from Azure Portal** 1. From Azure Home select the Portal Menu. 1. Select `Microsoft Defender for Cloud`. 1. Under `Management`, select `Environment Settings`. 1. Click on the appropriate Management Group, Subscription, or Workspace. 1. Click on `Email notifications`. 1. Ensure that a valid security contact email address is listed in the `Additional email addresses` field. **Audit from Azure CLI** Ensure the output of the below command is not empty and is set with appropriate email ids: ``` az account get-access-token --query {subscription:subscription,accessToken:accessToken} --out tsv | xargs -L1 bash -c 'curl -X GET -H Authorization: Bearer $1 -H Content-Type: application/json https://management.azure.com/subscriptions/$0/providers/Microsoft.Security/securityContacts?api-version=2020-01-01-preview' | jq '.|.[] | select(.name==default)'|jq '.properties.emails' ``` **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:** [4f4f78b8-e367-4b10-a341-d9a4ad5cf1c7](https://portal.azure.com/#view/Microsoft_Azure_Policy/PolicyDetailBlade/definitionId/%2Fproviders%2FMicrosoft.Authorization%2FpolicyDefinitions%2F4f4f78b8-e367-4b10-a341-d9a4ad5cf1c7) **- Name:** 'Subscriptions should have a contact email address for security issues'", + "AdditionalInformation": "Excluding any entries in the input.json properties block disables the specific setting by default.", + "References": "https://docs.microsoft.com/en-us/azure/security-center/security-center-provide-security-contact-details:https://docs.microsoft.com/en-us/rest/api/securitycenter/securitycontacts/list:https://docs.microsoft.com/en-us/rest/api/securitycenter/security-contacts:https://learn.microsoft.com/en-us/security/benchmark/azure/mcsb-incident-response#ir-2-preparation---setup-incident-notification", + "DefaultValue": "By default, there are no additional email addresses entered." + } + ] + }, + { + "Id": "8.1.14", + "Description": "Ensure that 'Notify about alerts with the following severity (or higher)' is enabled", + "Checks": [ + "defender_ensure_notify_alerts_severity_is_high" + ], + "Attributes": [ + { + "Section": "8 Security Services", + "SubSection": "8.1 Microsoft Defender for Cloud", + "Profile": "Level 1", + "AssessmentStatus": "Automated", + "Description": "Enables emailing security alerts to the subscription owner or other designated security contact.", + "RationaleStatement": "Enabling security alert emails ensures that security alert emails are sent by Microsoft. This ensures that the right people are aware of any potential security issues and can mitigate the risk.", + "ImpactStatement": "Enabling security alert emails can cause alert fatigue, increasing the risk of missing important alerts. Select an appropriate severity level to manage notifications. Azure aims to reduce alert fatigue by limiting the daily email volume per severity level. Learn more: https://learn.microsoft.com/en-us/azure/defender-for-cloud/configure-email-notifications#email-frequency.", + "RemediationProcedure": "**Remediate from Azure Portal** 1. From Azure Home select the Portal Menu. 1. Select `Microsoft Defender for Cloud`. 1. Under `Management`, select `Environment settings`. 1. Click on the appropriate Subscription. 1. Click on `Email notifications`. 1. Under `Notification types`, check box next to `Notify about alerts with the following severity (or higher)` and select an appropriate severity level from the drop-down menu. 1. Click `Save`. 1. Repeat steps 1-7 for each Subscription requiring remediation. **Remediate from Azure CLI** Use the below command to enable `Send email notification for high severity alerts`: ``` az account get-access-token --query {subscription:subscription,accessToken:accessToken} --out tsv | xargs -L1 bash -c 'curl -X PUT -H Authorization: Bearer $1 -H Content-Type: application/json https://management.azure.com/subscriptions/<$0>/providers/Microsoft.Security/securityContacts/default1?api-version=2017-08-01-preview -d@input.json' ``` Where `input.json` contains the data below, replacing `validEmailAddress` with a single email address or multiple comma-separated email addresses: ``` { id: /subscriptions//providers/Microsoft.Security/securityContacts/default, name: default, type: Microsoft.Security/securityContacts, properties: { email: , alertNotifications: On, alertsToAdmins: On } } ```", + "AuditProcedure": "**Audit from Azure Portal** 1. From Azure Home select the Portal Menu. 1. Select `Microsoft Defender for Cloud`. 1. Under `Management`, select `Environment settings`. 1. Click on the appropriate Subscription. 1. Click on `Email notifications`. 1. Under `Notification types`, ensure that the box next to `Notify about alerts with the following severity (or higher)` is checked, and an appropriate severity level is selected. 1. Repeat steps 1-6 for each Subscription. **Audit from Azure CLI** Including a Subscription ID at the `$0` in `/subscriptions/$0/providers`, ensure the below command returns `state: On`, and that `minimalSeverity` is set to an appropriate severity level: ``` az account get-access-token --query {subscription:subscription,accessToken:accessToken} --out tsv | xargs -L1 bash -c 'curl -X GET -H Authorization: Bearer $1 -H Content-Type: application/json https://management.azure.com/subscriptions/$0/providers/Microsoft.Security/securityContacts?api-version=2020-01-01-preview' | jq '.|.[] | select(.name==default)'|jq '.properties.alertNotifications' ``` **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:** [6e2593d9-add6-4083-9c9b-4b7d2188c899](https://portal.azure.com/#view/Microsoft_Azure_Policy/PolicyDetailBlade/definitionId/%2Fproviders%2FMicrosoft.Authorization%2FpolicyDefinitions%2F6e2593d9-add6-4083-9c9b-4b7d2188c899) **- Name:** 'Email notification for high severity alerts should be enabled'", + "AdditionalInformation": "Excluding any entries in the `input.json` properties block disables the specific setting by default. This recommendation has been updated to reflect recent changes to Microsoft REST APIs for getting and updating security contact information.", + "References": "https://docs.microsoft.com/en-us/azure/security-center/security-center-provide-security-contact-details:https://docs.microsoft.com/en-us/rest/api/securitycenter/security-contacts:https://docs.microsoft.com/en-us/rest/api/securitycenter/securitycontacts/list:https://learn.microsoft.com/en-us/security/benchmark/azure/mcsb-incident-response#ir-2-preparation---setup-incident-notification", + "DefaultValue": "By default, subscription owners receive email notifications for high-severity alerts." + } + ] + }, + { + "Id": "8.1.15", + "Description": "Ensure that 'Notify about attack paths with the following risk level (or higher)' is enabled", + "Checks": [ + "defender_attack_path_notifications_properly_configured" + ], + "Attributes": [ + { + "Section": "8 Security Services", + "SubSection": "8.1 Microsoft Defender for Cloud", + "Profile": "Level 1", + "AssessmentStatus": "Automated", + "Description": "Enables emailing attack paths to the subscription owner or other designated security contact.", + "RationaleStatement": "Enabling attack path emails ensures that attack path emails are sent by Microsoft. This ensures that the right people are aware of any potential security issues and can mitigate the risk.", + "ImpactStatement": "Enabling attack path emails can cause alert fatigue, increasing the risk of missing important alerts. Select an appropriate risk level to manage notifications. Azure aims to reduce alert fatigue by limiting the daily email volume per risk level. Learn more: https://learn.microsoft.com/en-us/azure/defender-for-cloud/configure-email-notifications#email-frequency.", + "RemediationProcedure": "**Remediate from Azure Portal** 1. From Azure Home select the Portal Menu. 1. Select `Microsoft Defender for Cloud`. 1. Under `Management`, select `Environment settings`. 1. Click on the appropriate Subscription. 1. Click on `Email notifications`. 1. Under Notification types, check the box next to `Notify about attack paths with the following risk level (or higher)`, and select an appropriate risk level from the drop-down menu. 1. Repeat steps 1-6 for each Subscription.", + "AuditProcedure": "**Audit from Azure Portal** 1. From Azure Home select the Portal Menu. 1. Select `Microsoft Defender for Cloud`. 1. Under `Management`, select `Environment settings`. 1. Click on the appropriate Subscription. 1. Click on `Email notifications`. 1. Under Notification types, ensure that the box next to `Notify about attack paths with the following risk level (or higher)` is checked, and an appropriate risk level is selected. 1. Repeat steps 1-6 for each Subscription. **Audit from Azure CLI** Including a Subscription ID at the `$0` in `/subscriptions/$0/providers`, ensure the below command returns `sourceType: AttackPath`, and that `minimalRiskLevel` is set to an appropriate risk level: ``` az account get-access-token --query {subscription:subscription,accessToken:accessToken} --out tsv | xargs -L1 bash -c 'curl -X GET -H Authorization: Bearer $1 -H Content-Type: application/json https://management.azure.com/subscriptions/$0/providers/Microsoft.Security/securityContacts?api-version=2023-12-01-preview' | jq '.|.[]' ```", + "AdditionalInformation": "", + "References": "https://learn.microsoft.com/en-us/azure/defender-for-cloud/configure-email-notifications:https://learn.microsoft.com/en-us/azure/defender-for-cloud/how-to-manage-attack-path:https://learn.microsoft.com/en-us/azure/defender-for-cloud/concept-attack-path", + "DefaultValue": "" + } + ] + }, + { + "Id": "8.1.16", + "Description": "Ensure that Microsoft Defender External Attack Surface Monitoring (EASM) is enabled", + "Checks": [], + "Attributes": [ + { + "Section": "8 Security Services", + "SubSection": "8.1 Microsoft Defender for Cloud", + "Profile": "Level 1", + "AssessmentStatus": "Manual", + "Description": "An organization's attack surface is the collection of assets with a public network identifier or URI that an external threat actor can see or access from outside your cloud. It is the set of points on the boundary of a system, a system element, system component, or an environment where an attacker can try to enter, cause an effect on, or extract data from, that system, system element, system component, or environment. The larger the attack surface, the harder it is to protect. This tool can be configured to scan your organization's online infrastructure such as specified domains, hosts, CIDR blocks, and SSL certificates, and store them in an Inventory. Inventory items can be added, reviewed, approved, and removed, and may contain enrichments (insights) and additional information collected from the tool's different scan engines and open-source intelligence sources. A Defender EASM workspace will generate an Inventory of publicly exposed assets by crawling and scanning the internet using _Seeds_ you provide when setting up the tool. Seeds can be FQDNs, IP CIDR blocks, and WHOIS records. Defender EASM will generate Insights within 24-48 hours after Seeds are provided, and these insights include vulnerability data (CVEs), ports and protocols, and weak or expired SSL certificates that could be used by an attacker for reconnaissance or exploitation. Results are classified High/Medium/Low and some of them include proposed mitigations.", + "RationaleStatement": "This tool can monitor the externally exposed resources of an organization, provide valuable insights, and export these findings in a variety of formats (including CSV) for use in vulnerability management operations and red/purple team exercises.", + "ImpactStatement": "Microsoft Defender EASM workspaces are currently available as Azure Resources with a 30-day free trial period but can quickly accrue significant charges. The costs are calculated daily as (Number of billable inventory items) x (item cost per day; approximately: $0.017). Estimated cost is not provided within the tool, and users are strongly advised to contact their Microsoft sales representative for pricing and set a calendar reminder for the end of the trial period. For an EASM workspace having an Inventory of 5k-10k billable items (IP addresses, hostnames, SSL certificates, etc) a typical cost might be approximately $85-170 per day or $2500-5000 USD/month at the time of publication. If the workspace is deleted by the last day of a free trial period, no charges are billed.", + "RemediationProcedure": "**Remediate from Azure Portal** 1. Go to `Microsoft Defender EASM`. 1. Click `+ Create`. 1. Under `Project details`, select a subscription. 1. Select or create a resource group. 1. Under `Instance details`, enter a name for the workspace. 1. Select a region. 1. Click `Review + create`. 1. Click `Create`. 1. Once the deployment has completed, go to `Microsoft Defender EASM`. 1. Click the workspace name. 1. Configure the workspace appropriately for your environment and organization.", + "AuditProcedure": "**Audit from Azure Portal** 1. Go to `Microsoft Defender EASM`. 1. Ensure that at least one Microsoft Defender EASM workspace is listed. 1. Click the name of a workspace. 1. Ensure the workspace is configured appropriately for your environment and organization. 1. Repeat steps 3-4 for each workspace.", + "AdditionalInformation": "Microsoft added its Defender for External Attack Surface management (EASM) offering to Azure following its 2022 acquisition of EASM SaaS tool company RiskIQ.", + "References": "https://learn.microsoft.com/en-us/azure/external-attack-surface-management/:https://learn.microsoft.com/en-us/azure/external-attack-surface-management/deploying-the-defender-easm-azure-resource:https://www.microsoft.com/en-us/security/blog/2022/08/02/microsoft-announces-new-solutions-for-threat-intelligence-and-attack-surface-management/", + "DefaultValue": "Microsoft Defender EASM is an optional, paid Azure Resource that must be created and configured inside a Subscription and Resource Group." + } + ] + }, + { + "Id": "8.2.1", + "Description": "Ensure That Microsoft Defender for IoT Hub Is Set To 'On'", + "Checks": [ + "defender_ensure_iot_hub_defender_is_on" + ], + "Attributes": [ + { + "Section": "8 Security Services", + "SubSection": "8.2 Microsoft Defender for IoT", + "Profile": "Level 1", + "AssessmentStatus": "Manual", + "Description": "Microsoft Defender for IoT acts as a central security hub for IoT devices within your organization.", + "RationaleStatement": "IoT devices are very rarely patched and can be potential attack vectors for enterprise networks. Updating their network configuration to use a central security hub allows for detection of these breaches.", + "ImpactStatement": "Enabling Microsoft Defender for IoT will incur additional charges dependent on the level of usage.", + "RemediationProcedure": "**Remediate from Azure Portal** 1. Go to `IoT Hub`. 2. Select an `IoT Hub` to validate. 3. Select `Overview` in `Defender for IoT`. 4. Click on `Secure your IoT solution`, and complete the onboarding.", + "AuditProcedure": "**Audit from Azure Portal** 1. Go to `IoT Hub`. 2. Select an `IoT Hub` to validate. 3. Select `Overview` in `Defender for IoT`. 4. The Threat prevention and Threat detection screen will appear, if `Defender for IoT` is Enabled.", + "AdditionalInformation": "There are additional configurations for Microsoft Defender for IoT that allow for types of deployments called hybrid or local. Both run on your physical infrastructure. These are complicated setups and are primarily outside of the scope of a purely Azure benchmark. Please see the references to consider these options for your organization.", + "References": "https://azure.microsoft.com/en-us/services/iot-defender/#overview:https://docs.microsoft.com/en-us/azure/defender-for-iot/:https://azure.microsoft.com/en-us/pricing/details/iot-defender/:https://docs.microsoft.com/en-us/security/benchmark/azure/baselines/defender-for-iot-security-baseline:https://docs.microsoft.com/en-us/cli/azure/iot?view=azure-cli-latest:https://learn.microsoft.com/en-us/security/benchmark/azure/mcsb-logging-threat-detection#lt-1-enable-threat-detection-capabilities:https://learn.microsoft.com/en-us/azure/defender-for-iot/device-builders/quickstart-onboard-iot-hub", + "DefaultValue": "By default, Microsoft Defender for IoT is not enabled." + } + ] + }, + { + "Id": "8.3.1", + "Description": "Ensure that the Expiration Date is set for all Keys in RBAC Key Vaults", + "Checks": [ + "keyvault_rbac_key_expiration_set" + ], + "Attributes": [ + { + "Section": "8 Security Services", + "SubSection": "8.3 Key Vault", + "Profile": "Level 1", + "AssessmentStatus": "Automated", + "Description": "Ensure that all Keys in Role Based Access Control (RBAC) Azure Key Vaults have an expiration date set.", + "RationaleStatement": "Azure Key Vault enables users to store and use cryptographic keys within the Microsoft Azure environment. The `exp` (expiration date) attribute identifies the expiration date on or after which the key MUST NOT be used for encryption of new data, wrapping of new keys, and signing. By default, keys never expire. It is thus recommended that keys be rotated in the key vault and set an explicit expiration date for all keys to help enforce the key rotation. This ensures that the keys cannot be used beyond their assigned lifetimes.", + "ImpactStatement": "Keys cannot be used beyond their assigned expiration dates respectively. Keys need to be rotated periodically wherever they are used.", + "RemediationProcedure": "**Remediate from Azure Portal** 1. Go to `Key vaults`. 2. For each Key vault, click on `Keys`. 3. In the main pane, ensure that an appropriate `Expiration date` is set for any keys that are `Enabled`. **Remediate from Azure CLI** Update the `Expiration date` for the key using the below command: ``` az keyvault key set-attributes --name --vault-name --expires Y-m-d'T'H:M:S'Z' ``` **Note:** To view the expiration date on all keys in a Key Vault using Microsoft API, the List Key permission is required. To update the expiration date for the keys: 1. Go to the Key vault, click on Access Control (IAM). 2. Click on Add role assignment and assign the role of Key Vault Crypto Officer to the appropriate user. **Remediate from PowerShell** ``` Set-AzKeyVaultKeyAttribute -VaultName -Name -Expires ```", + "AuditProcedure": "**Audit from Azure Portal** 1. Go to `Key vaults`. 2. For each Key vault, click on `Keys`. 3. In the main pane, ensure that an appropriate `Expiration date` is set for any keys that are `Enabled`. **Audit from Azure CLI** Get a list of all the key vaults in your Azure environment by running the following command: ``` az keyvault list ``` Then for each key vault listed ensure that the output of the below command contains Key ID (kid), enabled status as `true` and Expiration date (expires) is not empty or null: ``` az keyvault key list --vault-name --query '[*].{kid:kid,enabled:attributes.enabled,expires:attributes.expires}' ``` **Audit from PowerShell** Retrieve a list of Azure Key vaults: ``` Get-AzKeyVault ``` For each Key vault run the following command to determine which vaults are configured to use RBAC. ``` Get-AzKeyVault -VaultName ``` For each Key vault with the `EnableRbacAuthorizatoin` setting set to `True`, run the following command. ``` Get-AzKeyVaultKey -VaultName ``` Make sure the `Expires` setting is configured with a value as appropriate wherever the `Enabled` setting is set to `True`. **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:** [152b15f7-8e1f-4c1f-ab71-8c010ba5dbc0](https://portal.azure.com/#view/Microsoft_Azure_Policy/PolicyDetailBlade/definitionId/%2Fproviders%2FMicrosoft.Authorization%2FpolicyDefinitions%2F152b15f7-8e1f-4c1f-ab71-8c010ba5dbc0) **- Name:** 'Key Vault keys should have an expiration date'", + "AdditionalInformation": "", + "References": "https://docs.microsoft.com/en-us/azure/key-vault/key-vault-whatis:https://docs.microsoft.com/en-us/rest/api/keyvault/about-keys--secrets-and-certificates#key-vault-keys:https://learn.microsoft.com/en-us/security/benchmark/azure/mcsb-data-protection#dp-6-use-a-secure-key-management-process:https://docs.microsoft.com/en-us/powershell/module/az.keyvault/set-azkeyvaultkeyattribute?view=azps-0.10.0", + "DefaultValue": "By default, keys do not expire." + } + ] + }, + { + "Id": "8.3.2", + "Description": "Ensure that the Expiration Date is set for all Keys in Non-RBAC Key Vaults", + "Checks": [ + "keyvault_key_expiration_set_in_non_rbac" + ], + "Attributes": [ + { + "Section": "8 Security Services", + "SubSection": "8.3 Key Vault", + "Profile": "Level 1", + "AssessmentStatus": "Automated", + "Description": "Ensure that all Keys in Non Role Based Access Control (RBAC) Azure Key Vaults have an expiration date set.", + "RationaleStatement": "Azure Key Vault enables users to store and use cryptographic keys within the Microsoft Azure environment. The `exp` (expiration date) attribute identifies the expiration date on or after which the key MUST NOT be used for a cryptographic operation. By default, keys never expire. It is thus recommended that keys be rotated in the key vault and set an explicit expiration date for all keys. This ensures that the keys cannot be used beyond their assigned lifetimes.", + "ImpactStatement": "Keys cannot be used beyond their assigned expiration dates respectively. Keys need to be rotated periodically wherever they are used.", + "RemediationProcedure": "**Remediate from Azure Portal** 1. Go to `Key vaults`. 2. For each Key vault, click on `Keys`. 3. In the main pane, ensure that the status of the key is `Enabled`. 4. For each enabled key, ensure that an appropriate `Expiration date` is set. **Remediate from Azure CLI** Update the `Expiration date` for the key using the below command: ``` az keyvault key set-attributes --name --vault-name --expires Y-m-d'T'H:M:S'Z' ``` **Note:** To view the expiration date on all keys in a Key Vault using Microsoft API, the List Key permission is required. To update the expiration date for the keys: 1. Go to Key vault, click on `Access policies`. 2. Click on `Create` and add an access policy with the `Update` permission (in the Key Permissions - Key Management Operations section). **Remediate from PowerShell** ``` Set-AzKeyVaultKeyAttribute -VaultName -Name -Expires ```", + "AuditProcedure": "**Audit from Azure Portal** 1. Go to `Key vaults`. 2. For each Key vault, click on `Keys`. 3. In the main pane, ensure that the status of the key is `Enabled`. 4. For each enabled key, ensure that an appropriate `Expiration date` is set. **Audit from Azure CLI** Get a list of all the key vaults in your Azure environment by running the following command: ``` az keyvault list ``` For each key vault, ensure that the output of the below command contains Key ID (kid), enabled status as `true` and Expiration date (expires) is not empty or null: ``` az keyvault key list --vault-name --query '[*].{kid:kid,enabled:attributes.enabled,expires:attributes.expires}' ``` **Audit from PowerShell** Retrieve a list of Azure Key vaults: ``` Get-AzKeyVault ``` For each Key vault, run the following command to determine which vaults are configured to not use RBAC: ``` Get-AzKeyVault -VaultName ``` For each Key vault with the `EnableRbacAuthorizatoin` setting set to `False` or empty, run the following command. ``` Get-AzKeyVaultKey -VaultName ``` Make sure the `Expires` setting is configured with a value as appropriate wherever the `Enabled` setting is set to `True`. **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:** [152b15f7-8e1f-4c1f-ab71-8c010ba5dbc0](https://portal.azure.com/#view/Microsoft_Azure_Policy/PolicyDetailBlade/definitionId/%2Fproviders%2FMicrosoft.Authorization%2FpolicyDefinitions%2F152b15f7-8e1f-4c1f-ab71-8c010ba5dbc0) **- Name:** 'Key Vault keys should have an expiration date'", + "AdditionalInformation": "", + "References": "https://docs.microsoft.com/en-us/azure/key-vault/key-vault-whatis:https://docs.microsoft.com/en-us/rest/api/keyvault/about-keys--secrets-and-certificates#key-vault-keys:https://learn.microsoft.com/en-us/security/benchmark/azure/mcsb-data-protection#dp-6-use-a-secure-key-management-process:https://docs.microsoft.com/en-us/powershell/module/az.keyvault/set-azkeyvaultkeyattribute?view=azps-0.10.0", + "DefaultValue": "By default, keys do not expire." + } + ] + }, + { + "Id": "8.3.3", + "Description": "Ensure that the Expiration Date is set for all Secrets in RBAC Key Vaults", + "Checks": [ + "keyvault_rbac_secret_expiration_set" + ], + "Attributes": [ + { + "Section": "8 Security Services", + "SubSection": "8.3 Key Vault", + "Profile": "Level 1", + "AssessmentStatus": "Automated", + "Description": "Ensure that all Secrets in Role Based Access Control (RBAC) Azure Key Vaults have an expiration date set.", + "RationaleStatement": "The Azure Key Vault enables users to store and keep secrets within the Microsoft Azure environment. Secrets in the Azure Key Vault are octet sequences with a maximum size of 25k bytes each. The `exp` (expiration date) attribute identifies the expiration date on or after which the secret MUST NOT be used. By default, secrets never expire. It is thus recommended to rotate secrets in the key vault and set an explicit expiration date for all secrets. This ensures that the secrets cannot be used beyond their assigned lifetimes.", + "ImpactStatement": "Secrets cannot be used beyond their assigned expiry date respectively. Secrets need to be rotated periodically wherever they are used.", + "RemediationProcedure": "**Remediate from Azure Portal** 1. Go to `Key vaults`. 2. For each Key vault, click on `Secrets`. 3. In the main pane, ensure that the status of the secret is `Enabled`. 4. For each enabled secret, ensure that an appropriate `Expiration date` is set. **Remediate from Azure CLI** Update the Expiration date for the secret using the below command: ``` az keyvault secret set-attributes --name --vault-name --expires Y-m-d'T'H:M:S'Z' ``` Note: To view the expiration date on all secrets in a Key Vault using Microsoft API, the `List Secret` permission is required. To update the expiration date for the secrets: 1. Go to the Key vault, click on `Access Control (IAM)`. 2. Click on `Add role assignment` and assign the role of `Key Vault Secrets Officer` to the appropriate user. **Remediate from PowerShell** ``` Set-AzKeyVaultSecretAttribute -VaultName -Name -Expires ```", + "AuditProcedure": "**Audit from Azure Portal** 1. Go to `Key vaults`. 2. For each Key vault, click on `Secrets`. 3. In the main pane, ensure that the status of the secret is `Enabled`. 4. For each enabled secret, ensure that an appropriate `Expiration date` is set. **Audit from Azure CLI** Ensure that the output of the below command contains ID (id), enabled status as `true` and Expiration date (expires) is not empty or null: ``` az keyvault secret list --vault-name --query '[*].{kid:kid,enabled:attributes.enabled,expires:attributes.expires}' ``` **Audit from PowerShell** Retrieve a list of Key vaults: ``` Get-AzKeyVault ``` For each Key vault, run the following command to determine which vaults are configured to use RBAC: ``` Get-AzKeyVault -VaultName ``` For each Key vault with the `EnableRbacAuthorization` setting set to `True`, run the following command: ``` Get-AzKeyVaultSecret -VaultName ``` Make sure the `Expires` setting is configured with a value as appropriate wherever the `Enabled` setting is set to `True`. **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:** [98728c90-32c7-4049-8429-847dc0f4fe37](https://portal.azure.com/#view/Microsoft_Azure_Policy/PolicyDetailBlade/definitionId/%2Fproviders%2FMicrosoft.Authorization%2FpolicyDefinitions%2F98728c90-32c7-4049-8429-847dc0f4fe37) **- Name:** 'Key Vault secrets should have an expiration date'", + "AdditionalInformation": "", + "References": "https://docs.microsoft.com/en-us/azure/key-vault/key-vault-whatis:https://docs.microsoft.com/en-us/rest/api/keyvault/about-keys--secrets-and-certificates#key-vault-secrets:https://learn.microsoft.com/en-us/security/benchmark/azure/mcsb-data-protection#dp-6-use-a-secure-key-management-process:https://docs.microsoft.com/en-us/powershell/module/az.keyvault/set-azkeyvaultsecretattribute?view=azps-0.10.0", + "DefaultValue": "By default, secrets do not expire." + } + ] + }, + { + "Id": "8.3.4", + "Description": "Ensure that the Expiration Date is set for all Secrets in Non-RBAC Key Vaults", + "Checks": [ + "keyvault_non_rbac_secret_expiration_set" + ], + "Attributes": [ + { + "Section": "8 Security Services", + "SubSection": "8.3 Key Vault", + "Profile": "Level 1", + "AssessmentStatus": "Automated", + "Description": "Ensure that all Secrets in Non Role Based Access Control (RBAC) Azure Key Vaults have an expiration date set.", + "RationaleStatement": "The Azure Key Vault enables users to store and keep secrets within the Microsoft Azure environment. Secrets in the Azure Key Vault are octet sequences with a maximum size of 25k bytes each. The `exp` (expiration date) attribute identifies the expiration date on or after which the secret MUST NOT be used. By default, secrets never expire. It is thus recommended to rotate secrets in the key vault and set an explicit expiration date for all secrets. This ensures that the secrets cannot be used beyond their assigned lifetimes.", + "ImpactStatement": "Secrets cannot be used beyond their assigned expiry date respectively. Secrets need to be rotated periodically wherever they are used.", + "RemediationProcedure": "**Remediate from Azure Portal** 1. Go to `Key vaults`. 2. For each Key vault, click on `Secrets`. 3. In the main pane, ensure that the status of the secret is `Enabled`. 4. Set an appropriate `Expiration date` on all secrets. **Remediate from Azure CLI** Update the `Expiration date` for the secret using the below command: ``` az keyvault secret set-attributes --name --vault-name --expires Y-m-d'T'H:M:S'Z' ``` Note: To view the expiration date on all secrets in a Key Vault using Microsoft API, the `List` Secret permission is required. To update the expiration date for the secrets: 1. Go to Key vault, click on `Access policies`. 2. Click on `Create` and add an access policy with the `Update` permission (in the Secret Permissions - Secret Management Operations section). **Remediate from PowerShell** For each Key vault with the `EnableRbacAuthorization` setting set to `False` or empty, run the following command. ``` Set-AzKeyVaultSecret -VaultName -Name -Expires ```", + "AuditProcedure": "**Audit from Azure Portal** 1. Go to `Key vaults`. 2. For each Key vault, click on `Secrets`. 3. In the main pane, ensure that the status of the secret is `Enabled`. 4. Set an appropriate `Expiration date` on all secrets. **Audit from Azure CLI** Get a list of all the key vaults in your Azure environment by running the following command: ``` az keyvault list ``` For each key vault, ensure that the output of the below command contains ID (id), enabled status as `true` and Expiration date (expires) is not empty or null: ``` az keyvault secret list --vault-name --query '[*].{kid:kid,enabled:attributes.enabled,expires:attributes.expires}' ``` **Audit from PowerShell** Retrieve a list of Key vaults: ``` Get-AzKeyVault ``` For each Key vault run the following command to determine which vaults are configured to use RBAC: ``` Get-AzKeyVault -VaultName ``` For each Key Vault with the `EnableRbacAuthorization` setting set to `False` or empty, run the following command. ``` Get-AzKeyVaultSecret -VaultName ``` Make sure the `Expires` setting is configured with a value as appropriate wherever the `Enabled` setting is set to `True`. **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:** [98728c90-32c7-4049-8429-847dc0f4fe37](https://portal.azure.com/#view/Microsoft_Azure_Policy/PolicyDetailBlade/definitionId/%2Fproviders%2FMicrosoft.Authorization%2FpolicyDefinitions%2F98728c90-32c7-4049-8429-847dc0f4fe37) **- Name:** 'Key Vault secrets should have an expiration date'", + "AdditionalInformation": "", + "References": "https://docs.microsoft.com/en-us/azure/key-vault/key-vault-whatis:https://docs.microsoft.com/en-us/rest/api/keyvault/about-keys--secrets-and-certificates#key-vault-secrets:https://learn.microsoft.com/en-us/security/benchmark/azure/mcsb-data-protection#dp-6-use-a-secure-key-management-process:https://docs.microsoft.com/en-us/powershell/module/az.keyvault/set-azkeyvaultsecret?view=azps-7.4.0", + "DefaultValue": "By default, secrets do not expire." + } + ] + }, + { + "Id": "8.3.5", + "Description": "Ensure 'Purge protection' is set to 'Enabled'", + "Checks": [ + "keyvault_recoverable" + ], + "Attributes": [ + { + "Section": "8 Security Services", + "SubSection": "8.3 Key Vault", + "Profile": "Level 1", + "AssessmentStatus": "Automated", + "Description": "Key vaults contain object keys, secrets, and certificates. Deletion of a key vault can cause immediate data loss or loss of security functions (authentication, validation, verification, non-repudiation, etc.) supported by the key vault objects. It is recommended the key vault be made recoverable by enabling the 'purge protection' function. This is to prevent the loss of encrypted data, including storage accounts, SQL databases, and/or dependent services provided by key vault objects (keys, secrets, certificates, etc.). NOTE: In February 2025, Microsoft enabled soft delete protection on all key vaults. Users can no longer opt out of or turn off soft delete. WARNING: A current limitation is that role assignments disappear when a key vault is deleted. All role assignments will need to be recreated after recovery.", + "RationaleStatement": "Users may accidentally run delete/purge commands on a key vault, or an attacker or malicious user may do so deliberately in order to cause disruption. Deleting or purging a key vault leads to immediate data loss, as keys encrypting data and secrets/certificates allowing access/services will become inaccessible. Enabling purge protection ensures that even if a key vault is deleted, the key vault and its objects remain recoverable during the configurable retention period. If no action is taken, the key vault and its objects will be purged once the retention period elapses.", + "ImpactStatement": "Once purge protection is enabled for a key vault, it cannot be disabled.", + "RemediationProcedure": "**Note:** Once enabled, purge protection cannot be disabled. **Remediate from Azure Portal** 1. Go to `Key Vaults`. 2. Click the name of a key vault. 3. Under `Settings`, click `Properties`. 4. Select the radio button next to `Enable purge protection (enforce a mandatory retention period for deleted vaults and vault objects)`. 5. Click `Save`. 6. Repeat steps 1-5 for each key vault requiring remediation. **Remediate from Azure CLI** For each key vault requiring remediation, run the following command to enable purge protection: ``` az resource update --resource-group --name --resource-type \"Microsoft.KeyVault/vaults\" --set properties.enablePurgeProtection=true ``` **Remediate from PowerShell** For each key vault requiring remediation, run the following command to enable purge protection: ``` Update-AzKeyVault -ResourceGroupName -VaultName -EnablePurgeProtection ```", + "AuditProcedure": "**Audit from Azure Portal** 1. Go to `Key Vaults`. 2. Click the name of a key vault. 3. Under `Settings`, click `Properties`. 4. Next to `Purge protection`, ensure that `Enable purge protection (enforce a mandatory retention period for deleted vaults and vault objects)` is selected. 5. Repeat steps 1-4 for each key vault. **Audit from Azure CLI** Run the following command to list key vaults: ``` az resource list --query \"[?type=='Microsoft.KeyVault/vaults']\" ``` For each key vault, run the following command to get the purge protection setting: ``` az resource show --resource-group --name --resource-type \"Microsoft.KeyVault/vaults\" --query properties.enablePurgeProtection ``` Ensure that `true` is returned. **Audit from PowerShell** Run the following command to list key vaults: ``` Get-AzKeyVault ``` For each key vault, run the following command to get the key vault details: ``` Get-AzKeyVault -ResourceGroupName -VaultName ``` Ensure `Purge Protection Enabled?` is set to `True`. **Audit from Azure Policy** - **Policy ID:** 0b60c0b2-2dc2-4e1c-b5c9-abbed971de53 - **Name:** 'Key vaults should have deletion protection enabled'", + "AdditionalInformation": "", + "References": "https://learn.microsoft.com/en-us/azure/key-vault/general/key-vault-recovery:https://learn.microsoft.com/en-us/security/benchmark/azure/mcsb-governance-strategy#gs-8-define-and-implement-backup-and-recovery-strategy:https://learn.microsoft.com/en-us/security/benchmark/azure/mcsb-data-protection#dp-8-ensure-security-of-key-and-certificate-repository", + "DefaultValue": "Purge protection is disabled by default." + } + ] + }, + { + "Id": "8.3.6", + "Description": "Ensure that Role Based Access Control for Azure Key Vault is enabled", + "Checks": [ + "keyvault_rbac_enabled" + ], + "Attributes": [ + { + "Section": "8 Security Services", + "SubSection": "8.3 Key Vault", + "Profile": "Level 1", + "AssessmentStatus": "Automated", + "Description": "The recommended way to access Key Vaults is to use the Azure Role-Based Access Control (RBAC) permissions model. Azure RBAC is an authorization system built on Azure Resource Manager that provides fine-grained access management of Azure resources. It allows users to manage Key, Secret, and Certificate permissions. It provides one place to manage all permissions across all key vaults.", + "RationaleStatement": "The new RBAC permissions model for Key Vaults enables a much finer grained access control for key vault secrets, keys, certificates, etc., than the vault access policy. This in turn will permit the use of privileged identity management over these roles, thus securing the key vaults with JIT Access management.", + "ImpactStatement": "Implementation needs to be properly designed from the ground up, as this is a fundamental change to the way key vaults are accessed/managed. Changing permissions to key vaults will result in loss of service as permissions are re-applied. For the least amount of downtime, map your current groups and users to their corresponding permission needs.", + "RemediationProcedure": "**Remediate from Azure Portal** Key Vaults can be configured to use `Azure role-based access control` on creation. For existing Key Vaults: 1. From Azure Home open the Portal Menu in the top left corner 2. Select `Key Vaults` 3. Select a Key Vault to audit 4. Select `Access configuration` 5. Set the Permission model radio button to `Azure role-based access control`, taking note of the warning message 6. Click `Save` 7. Select `Access Control (IAM)` 8. Select the `Role Assignments` tab 9. Reapply permissions as needed to groups or users **Remediate from Azure CLI** To enable RBAC Authorization for each Key Vault, run the following Azure CLI command: ``` az keyvault update --resource-group --name --enable-rbac-authorization true ``` **Remediate from PowerShell** To enable RBAC authorization on each Key Vault, run the following PowerShell command: ``` Update-AzKeyVault -ResourceGroupName -VaultName -EnableRbacAuthorization $True ```", + "AuditProcedure": "**Audit from Azure Portal** 1. From Azure Home open the Portal Menu in the top left corner 2. Select Key Vaults 3. Select a Key Vault to audit 4. Select Access configuration 5. Ensure the Permission Model radio button is set to `Azure role-based access control` **Audit from Azure CLI** Run the following command for each Key Vault in each Resource Group: ``` az keyvault show --resource-group --name ``` Ensure the `enableRbacAuthorization` setting is set to `true` within the output of the above command. **Audit from PowerShell** Run the following PowerShell command: ``` Get-AzKeyVault -Vaultname -ResourceGroupName ``` Ensure the `Enabled For RBAC Authorization` setting is set to `True` **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:** [12d4fa5e-1f9f-4c21-97a9-b99b3c6611b5](https://portal.azure.com/#view/Microsoft_Azure_Policy/PolicyDetailBlade/definitionId/%2Fproviders%2FMicrosoft.Authorization%2FpolicyDefinitions%2F12d4fa5e-1f9f-4c21-97a9-b99b3c6611b5) **- Name:** 'Azure Key Vault should use RBAC permission model'", + "AdditionalInformation": "", + "References": "https://docs.microsoft.com/en-gb/azure/key-vault/general/rbac-migration#vault-access-policy-to-azure-rbac-migration-steps:https://docs.microsoft.com/en-gb/azure/role-based-access-control/role-assignments-portal?tabs=current:https://docs.microsoft.com/en-gb/azure/role-based-access-control/overview:https://learn.microsoft.com/en-us/security/benchmark/azure/mcsb-data-protection#dp-8-ensure-security-of-key-and-certificate-repository", + "DefaultValue": "The default value for Access control in Key Vaults is Vault Policy." + } + ] + }, + { + "Id": "8.3.7", + "Description": "Ensure Public Network Access is Disabled", + "Checks": [ + "keyvault_private_endpoints" + ], + "Attributes": [ + { + "Section": "8 Security Services", + "SubSection": "8.3 Key Vault", + "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. When a private endpoint is configured on a key vault, connections from Azure resources within the same subnet will use its private IP address. However, network traffic from the public internet can still connect to the key vault's public endpoint (mykeyvault.vault.azure.net) using its public IP address unless public network access is disabled. Disabling public network access removes the vault's public endpoint from Azure public DNS, reducing its exposure to the public internet. With a private endpoint configured, network traffic will use the vault's private endpoint IP address for all requests (mykeyvault.vault.privatelink.azure.net).", + "RationaleStatement": "Disabling public network access improves security by ensuring that a service is not exposed on the public internet. Removing a point of interconnection from the internet edge to your key vault can strengthen the network security boundary of your system and reduce the risk of exposing the control plane or vault objects to untrusted clients. Although Azure resources are never truly isolated from the public internet, disabling the public endpoint removes a line of sight from the public internet and increases the effort required for an attack.", + "ImpactStatement": "NOTE: Prior to disabling public network access, it is strongly recommended that, for each key vault, either: virtual network integration is completed OR private endpoints/links are set up as described in 'Ensure Private Endpoints are used to access Azure Key Vault.' Disabling public network access restricts access to the service. This enhances security but 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 `Key vaults`. 2. Click the name of a key vault. 3. Under `Settings`, click `Networking`. 4. Under `Firewalls and virtual networks`, next to `Allow access from:`, click the radio button next to `Disable public access`. 5. Click `Apply`. 6. Repeat steps 1-5 for each key vault requiring remediation. **Remediate from Azure CLI** For each key vault requiring remediation, run the following command to disable public network access: ``` az keyvault update --resource-group --name --public-network-access Disabled ``` **Remediate from PowerShell** For each key vault requiring remediation, run the following command to disable public network access: ``` Update-AzKeyVault -ResourceGroupName -VaultName -PublicNetworkAccess \"Disabled\" ```", + "AuditProcedure": "**Audit from Azure Portal** 1. Go to `Key vaults`. 2. Click the name of a key vault. 3. Under `Settings`, click `Networking`. 4. Under `Firewalls and virtual networks`, ensure that `Allow access from:` is set to `Disable public access`. 5. Repeat steps 1-4 for each key vault. **Audit from Azure CLI** Run the following command to list key vaults: ``` az keyvault list ``` For each key vault, run the following command to get the public network access setting: ``` az keyvault show --resource-group --name --query properties.publicNetworkAccess ``` Ensure that `Disabled` is returned. **Audit from PowerShell** Run the following command to list key vaults: ``` Get-AzKeyVault ``` Run the following command to get the key vault in a resource group with a given name: ``` $vault = Get-AzKeyVault -ResourceGroupName -Name ``` Run the following command to get the public network access setting for the key vault: ``` $vault.PublicNetworkAccess ``` Ensure that `Disabled` is returned. Repeat for each key vault. **Audit from Azure Policy** - **Policy ID:** 405c5871-3e91-4644-8a63-58e19d68ff5b - **Name:** 'Azure Key Vault should disable public network access'", + "AdditionalInformation": "This Common Reference Recommendation is referenced in the following Service Recommendations: - Storage Services > Storage Accounts > Networking > **Ensure that 'Public Network Access' is 'Disabled' for storage accounts**", + "References": "https://learn.microsoft.com/en-us/azure/key-vault/general/network-security:https://learn.microsoft.com/en-us/azure/key-vault/general/private-link-service", + "DefaultValue": "Public network access is enabled by default." + } + ] + }, + { + "Id": "8.3.8", + "Description": "Ensure Private Endpoints are used to access Azure Key Vault", + "Checks": [ + "keyvault_private_endpoints" + ], + "Attributes": [ + { + "Section": "8 Security Services", + "SubSection": "8.3 Key Vault", + "Profile": "Level 2", + "AssessmentStatus": "Automated", + "Description": "Private endpoints will secure network traffic from Azure Key Vault to the resources requesting secrets and keys.", + "RationaleStatement": "Private endpoints will keep network requests to Azure Key Vault limited to the endpoints attached to the resources that are whitelisted to communicate with each other. Assigning the Key Vault to a network without an endpoint will allow other resources on that network to view all traffic from the Key Vault to its destination. In spite of the complexity in configuration, this is recommended for high security secrets.", + "ImpactStatement": "Incorrect or poorly-timed changing of network configuration could result in service interruption. There are also additional costs tiers for running a private endpoint per petabyte or more of networking traffic.", + "RemediationProcedure": "**Please see the additional information about the requirements needed before starting this remediation procedure.** **Remediate from Azure Portal** 1. From Azure Home open the Portal Menu in the top left. 2. Select Key Vaults. 3. Select a Key Vault to audit. 4. Select `Networking` in the left column. 5. Select `Private endpoint connections` from the top row. 6. Select `+ Create`. 7. Select the subscription the Key Vault is within, and other desired configuration. 8. Select `Next`. 9. For resource type select `Microsoft.KeyVault/vaults`. 10. Select the Key Vault to associate the Private Endpoint with. 11. Select `Next`. 12. In the `Virtual Networking` field, select the network to assign the Endpoint. 13. Select other configuration options as desired, including an existing or new application security group. 14. Select `Next`. 15. Select the private DNS the Private Endpoints will use. 16. Select `Next`. 17. Optionally add `Tags`. 18. Select `Next : Review + Create`. 19. Review the information and select `Create`. Follow the Audit Procedure to determine if it has successfully applied. 20. Repeat steps 3-19 for each Key Vault. **Remediate from Azure CLI** 1. To create an endpoint, run the following command: ``` az network private-endpoint create --resource-group --subnet --name --private-connection-resource-id /subscriptions//resourceGroups//providers/Microsoft.KeyVault/vaults/ --group-ids vault --connection-name --location --manual-request ``` 2. To manually approve the endpoint request, run the following command: ``` az keyvault private-endpoint-connection approve --resource-group --vault-name –name ``` 3. Determine the Private Endpoint's IP address to connect the Key Vault to the Private DNS you have previously created: 4. Look for the property networkInterfaces then id; the value must be placed in the variable within step 7. ``` az network private-endpoint show -g -n ``` 5. Look for the property networkInterfaces then id; the value must be placed on in step 7. ``` az network nic show --ids ``` 6. Create a Private DNS record within the DNS Zone you created for the Private Endpoint: ``` az network private-dns record-set a add-record -g -z privatelink.vaultcore.azure.net -n -a ``` 7. nslookup the private endpoint to determine if the DNS record is correct: ``` nslookup .vault.azure.net nslookup .privatelink.vaultcore.azure.n ```", + "AuditProcedure": "**Audit from Azure Portal** 1. From Azure Home open the Portal Menu in the top left. 2. Select Key Vaults. 3. Select a Key Vault to audit. 4. Select `Networking` in the left column. 5. Select `Private endpoint connections` from the top row. 6. View if there is an endpoint attached. **Audit from Azure CLI** Run the following command within a subscription for each Key Vault you wish to audit. ``` az keyvault show --name ``` Ensure that `privateEndpointConnections` is not `null`. **Audit from PowerShell** Run the following command within a subscription for each Key Vault you wish to audit. ``` Get-AzPrivateEndpointConnection -PrivateLinkResourceId '/subscriptions//resourceGroups//providers/Microsoft.KeyVault/vaults//' ``` Ensure that the response contains details of a private endpoint. **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:** [a6abeaec-4d90-4a02-805f-6b26c4d3fbe9](https://portal.azure.com/#view/Microsoft_Azure_Policy/PolicyDetailBlade/definitionId/%2Fproviders%2FMicrosoft.Authorization%2FpolicyDefinitions%2Fa6abeaec-4d90-4a02-805f-6b26c4d3fbe9) **- Name:** 'Azure Key Vaults should use private link'", + "AdditionalInformation": "This recommendation assumes that you have created a Resource Group containing a Virtual Network that the services are already associated with and configured private DNS. A Bastion on the virtual network is also required, and the service to which you are connecting must already have a Private Endpoint. For information concerning the installation of these services, please see the attached documentation. Microsoft's own documentation lists the requirements as: A Key Vault. An Azure virtual network. A subnet in the virtual network. Owner or contributor permissions for both the Key Vault and the virtual network.", + "References": "https://docs.microsoft.com/en-us/azure/private-link/private-endpoint-overview:https://docs.microsoft.com/en-us/azure/storage/common/storage-private-endpoints:https://azure.microsoft.com/en-us/pricing/details/private-link/:https://docs.microsoft.com/en-us/azure/key-vault/general/private-link-service?tabs=portal:https://docs.microsoft.com/en-us/azure/virtual-network/quick-create-portal:https://docs.microsoft.com/en-us/azure/private-link/tutorial-private-endpoint-storage-portal:https://docs.microsoft.com/en-us/azure/bastion/bastion-overview:https://docs.microsoft.com/azure/dns/private-dns-getstarted-cli#create-an-additional-dns-record:https://learn.microsoft.com/en-us/security/benchmark/azure/mcsb-data-protection#dp-8-ensure-security-of-key-and-certificate-repository", + "DefaultValue": "By default, Private Endpoints are not enabled for any services within Azure." + } + ] + }, + { + "Id": "8.3.9", + "Description": "Ensure automatic key rotation is enabled within Azure Key Vault", + "Checks": [ + "keyvault_key_rotation_enabled" + ], + "Attributes": [ + { + "Section": "8 Security Services", + "SubSection": "8.3 Key Vault", + "Profile": "Level 1", + "AssessmentStatus": "Automated", + "Description": "Automated cryptographic key rotation in Key Vault allows users to configure Key Vault to automatically generate a new key version at a specified frequency. A key rotation policy can be defined for each individual key.", + "RationaleStatement": "Automatic key rotation reduces risk by ensuring that keys are rotated without manual intervention. Azure and NIST recommend that keys be rotated every two years or less. Refer to 'Table 1: Suggested cryptoperiods for key types' on page 46 of the following document for more information: https://nvlpubs.nist.gov/nistpubs/SpecialPublications/NIST.SP.800-57pt1r5.pdf.", + "ImpactStatement": "There is an additional cost for each scheduled key rotation.", + "RemediationProcedure": "**Note:** Azure CLI and PowerShell use the ISO8601 duration format for time spans. The format is `P(Y,M,D)`. The leading P is required and is referred to as `period`. The `(Y,M,D)` are for the duration of Year, Month, and Day, respectively. A time frame of 2 years, 2 months, 2 days would be `P2Y2M2D`. For Azure CLI and PowerShell, it is easiest to supply the policy flags in a `.json file`, for example: ``` { lifetimeActions: [ { trigger: { timeAfterCreate: P(Y,M,D), timeBeforeExpiry : null }, action: { type: Rotate } }, { trigger: { timeBeforeExpiry : P(Y,M,D) }, action: { type: Notify } } ], attributes: { expiryTime: P(Y,M,D) } } ``` **Remediate from Azure Portal** 1. Go to `Key Vaults`. 1. Select a Key Vault. 1. Under `Objects`, select `Keys`. 1. Select a key. 1. From the top row, select `Rotation policy`. 1. Select an appropriate `Expiry time`. 1. Set `Enable auto rotation` to `Enabled`. 1. Set an appropriate `Rotation option` and `Rotation time`. 1. Optionally, set a `Notification time`. 1. Click `Save`. 1. Repeat steps 1-10 for each Key Vault and Key. **Remediate from Azure CLI** Run the following command for each key to enable automatic rotation: ``` az keyvault key rotation-policy update --vault-name --name --value ``` **Remediate from PowerShell** Run the following command for each key to enable automatic rotation: ``` Set-AzKeyVaultKeyRotationPolicy -VaultName -Name -PolicyPath ```", + "AuditProcedure": "**Audit from Azure Portal** 1. Go to `Key Vaults`. 1. Select a Key Vault. 1. Under `Objects`, select `Keys`. 1. Select a key. 1. From the top row, select `Rotation policy`. 1. Ensure `Enable auto rotation` is set to `Enabled`. 1. Ensure the `Rotation time` is set to an appropriate value. 1. Repeat steps 1-7 for each Key Vault and Key. **Audit from Azure CLI** Run the following command: ``` az keyvault key rotation-policy show --vault-name --name ``` Ensure that the response contains a `lifetimeAction` of `Rotate` and that `timeAfterCreate` is set to an appropriate value. **Audit from PowerShell** Run the following command: ``` Get-AzKeyVaultKeyRotationPolicy -VaultName -Name ``` Ensure that the response contains a `LifetimeAction` of `Rotate` and that `TimeAfterCreate` is set to an appropriate value. **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:** [d8cf8476-a2ec-4916-896e-992351803c44](https://portal.azure.com/#view/Microsoft_Azure_Policy/PolicyDetailBlade/definitionId/%2Fproviders%2FMicrosoft.Authorization%2FpolicyDefinitions%2Fd8cf8476-a2ec-4916-896e-992351803c44) **- Name:** 'Keys should have a rotation policy ensuring that their rotation is scheduled within the specified number of days after creation.'", + "AdditionalInformation": "", + "References": "https://docs.microsoft.com/en-us/azure/key-vault/keys/how-to-configure-key-rotation:https://docs.microsoft.com/en-us/azure/storage/common/customer-managed-keys-overview#update-the-key-version:https://docs.microsoft.com/en-us/azure/virtual-machines/windows/disks-enable-customer-managed-keys-powershell#set-up-an-azure-key-vault-and-diskencryptionset-optionally-with-automatic-key-rotation:https://azure.microsoft.com/en-us/updates/public-preview-automatic-key-rotation-of-customermanaged-keys-for-encrypting-azure-managed-disks/:https://docs.microsoft.com/en-us/cli/azure/keyvault/key/rotation-policy?view=azure-cli-latest#az-keyvault-key-rotation-policy-update:https://docs.microsoft.com/en-us/powershell/module/az.keyvault/set-azkeyvaultkeyrotationpolicy?view=azps-8.1.0:https://docs.microsoft.com/en-us/azure/data-explorer/kusto/query/scalar-data-types/timespan:https://learn.microsoft.com/en-us/security/benchmark/azure/mcsb-data-protection#dp-6-use-a-secure-key-management-process:https://nvlpubs.nist.gov/nistpubs/SpecialPublications/NIST.SP.800-57pt1r5.pdf", + "DefaultValue": "By default, automatic key rotation is not enabled." + } + ] + }, + { + "Id": "8.3.10", + "Description": "Ensure that Azure Key Vault Managed HSM is used when required", + "Checks": [], + "Attributes": [ + { + "Section": "8 Security Services", + "SubSection": "8.3 Key Vault", + "Profile": "Level 2", + "AssessmentStatus": "Manual", + "Description": "Azure Key Vault Managed HSM is a fully managed, highly available, single-tenant cloud service that safeguards cryptographic keys using FIPS 140-2 Level 3 validated HSMs. **Note:** This recommendation to use Managed HSM applies only to scenarios where specific regulatory and compliance requirements mandate the use of a dedicated hardware security module.", + "RationaleStatement": "Managed HSM is a fully managed, highly available, single-tenant service that ensures FIPS 140-2 Level 3 compliance. It provides centralized key management, isolated access control, and private endpoints for secure access. Integrated with Azure services, it supports migration from Key Vault, ensures data residency, and offers monitoring and auditing for enhanced security.", + "ImpactStatement": "Managed HSM incurs a cost of $0.40 to $5 per month for each actively used HSM-protected key, depending on the key type and quantity. Each key version is billed separately. Additionally, there is an hourly usage fee of $3.20 per Managed HSM pool.", + "RemediationProcedure": "**Remediate from Azure CLI** Run the following command to set `oid` to be the `OID` of the signed-in user: ``` $oid = az ad signed-in-user show --query id -o tsv ``` Alternatively, prepare a space-separated list of OIDs to be provided as the `administrators` of the HSM. Run the following command to create a Managed HSM: ``` az keyvault create --resource-group --hsm-name --retention-days --administrators $oid ``` The command can take several minutes to complete. After the HSM has been created, it must be activated before it can be used. Activation requires providing a minimum of three and a maximum of ten RSA key pairs, as well as the minimum number of keys required to decrypt the security domain (called a quorum). OpenSSL can be used to generate the self-signed certificates, for example: ``` openssl req -newkey rsa:2048 -nodes -keyout cert_1.key -x509 -days 365 -out cert_1.cer ``` Run the following command to download the security domain and activate the Managed HSM: ``` az keyvault security-domain download --hsm-name --sd-wrapping-keys --sd-quorum --security-domain-file .json ``` Store the security domain file and the RSA key pairs securely. They will be required for disaster recovery or for creating another Managed HSM that shares the same security domain so that the two can share keys. The Managed HSM will now be in an active state and ready for use.", + "AuditProcedure": "**Audit from Azure CLI** Run the following command to list key vaults: ``` az keyvault list --query [*].[name,type] ``` Ensure that at least one key vault with type `Microsoft.KeyVault/managedHSMs` exists.", + "AdditionalInformation": "", + "References": "https://learn.microsoft.com/en-us/azure/security/fundamentals/key-management-choose:https://learn.microsoft.com/en-us/azure/key-vault/managed-hsm/overview:https://azure.microsoft.com/en-gb/pricing/details/key-vault/:https://learn.microsoft.com/en-us/azure/key-vault/managed-hsm/quick-create-cli:https://learn.microsoft.com/en-us/cli/azure/keyvault", + "DefaultValue": "" + } + ] + }, + { + "Id": "8.3.11", + "Description": "Ensure certificate 'Validity Period (in months)' is less than or equal to '12'", + "Checks": [], + "Attributes": [ + { + "Section": "8 Security Services", + "SubSection": "8.3 Key Vault", + "Profile": "Level 1", + "AssessmentStatus": "Automated", + "Description": "Restrict the validity period of certificates stored in Azure Key Vault to 12 months or less.", + "RationaleStatement": "Limiting certificate validity reduces the risk of misuse if compromised and helps ensure timely renewal, improving security and reliability.", + "ImpactStatement": "Minor administrative effort required to ensure certificate renewal and lifecycle management.", + "RemediationProcedure": "**Remediate from Azure Portal** 1. Go to `Key vaults`. 2. Click the name of a key vault. 3. Under `Objects`, click `Certificates`. 4. Click the name of a certificate. 5. Click `Issuance Policy`. 6. Set `Validity Period (in months)` to an integer between 1 and 12, inclusive. 7. Click `Save`. 8. Repeat steps 1-7 for each key vault and certificate requiring remediation. **Remediate from PowerShell** For each certificate requiring remediation, run the following command to set ValidityInMonths to an integer between 1 and 12, inclusive: ``` Set-AzKeyVaultCertificatePolicy -VaultName $vault.VaultName -Name -ValidityInMonths ```", + "AuditProcedure": "**Audit from Azure Portal** 1. Go to `Key vaults`. 2. Click the name of a key vault. 3. Under `Objects`, click `Certificates`. 4. Click the name of a certificate. 5. Click `Issuance Policy`. 6. Ensure that `Validity Period (in months)` is set to 12 or less. 7. Repeat steps 1-6 for each key vault and certificate. **Audit from Azure CLI** Run the following command to list key vaults: ``` az keyvault list ``` For each key vault, run the following command to list certificates: ``` az keyvault certificate list --vault-name ``` For each certificate, run the following command to get the certificate policy's validityInMonths setting: ``` az keyvault certificate show --id --query policy.x509CertificateProperties.validityInMonths ``` Ensure that 12 or less is returned. **Audit from PowerShell** Run the following command to list key vaults: ``` Get-AzKeyVault ``` Run the following command to get the key vault with a given name: ``` $vault = Get-AzKeyVault -Name ``` Run the following command to list certificates in the key vault: ``` Get-AzKeyVaultCertificate -VaultName $vault.VaultName ``` Run the following command to get the policy of a certificate with a given name: ``` $certificate = Get-AzKeyVaultCertificatePolicy -VaultName $vault.VaultName -Name ``` Run the following command to get the certificate policy's ValidityInMonths setting: ``` $certificate.ValidityInMonths ``` Ensure that 12 or less is returned. Repeat for each key vault and certificate. **Audit from Azure Policy** - **Policy ID:** 0a075868-4c26-42ef-914c-5bc007359560 - **Name:** 'Certificates should have the specified maximum validity period'", + "AdditionalInformation": "", + "References": "https://learn.microsoft.com/en-us/azure/key-vault/certificates/about-certificates:https://learn.microsoft.com/en-us/cli/azure/keyvault:https://learn.microsoft.com/en-us/powershell/module/az.keyvault", + "DefaultValue": "Validity Period (in months) is set to 12 by default." + } + ] + }, + { + "Id": "8.4.1", + "Description": "Ensure an Azure Bastion Host Exists", + "Checks": [ + "network_bastion_host_exists" + ], + "Attributes": [ + { + "Section": "8 Security Services", + "SubSection": "8.4 Azure Bastion", + "Profile": "Level 1", + "AssessmentStatus": "Automated", + "Description": "The Azure Bastion service allows secure remote access to Azure Virtual Machines over the Internet without exposing remote access protocol ports and services directly to the Internet. The Azure Bastion service provides this access using TLS over 443/TCP, and subscribes to hardened configurations within an organization's Azure Active Directory service.", + "RationaleStatement": "The Azure Bastion service allows organizations a more secure means of accessing Azure Virtual Machines over the Internet without assigning public IP addresses to those Virtual Machines. The Azure Bastion service provides Remote Desktop Protocol (RDP) and Secure Shell (SSH) access to Virtual Machines using TLS within a web browser, thus preventing organizations from opening up 3389/TCP and 22/TCP to the Internet on Azure Virtual Machines. Additional benefits of the Bastion service includes Multi-Factor Authentication, Conditional Access Policies, and any other hardening measures configured within Azure Active Directory using a central point of access.", + "ImpactStatement": "The Azure Bastion service incurs additional costs and requires a specific virtual network configuration. The `Standard` tier offers additional configuration options compared to the `Basic` tier and may incur additional costs for those added features.", + "RemediationProcedure": "**Remediate from Azure Portal** 1. Click on `Bastions` 2. Select the `Subscription` 3. Select the `Resource group` 4. Type a `Name` for the new Bastion host 5. Select a `Region` 6. Choose `Standard` next to `Tier` 7. Use the slider to set the `Instance count` 8. Select the `Virtual network` or `Create new` 9. Select the `Subnet` named `AzureBastionSubnet`. Create a `Subnet` named `AzureBastionSubnet` using a `/26` CIDR range if it doesn't already exist. 10. Select the appropriate `Public IP address` option. 11. If `Create new` is selected for the `Public IP address` option, provide a `Public IP address name`. 12. If `Use existing` is selected for `Public IP address` option, select an IP address from `Choose public IP address` 13. Click `Next: Tags >` 14. Configure the appropriate `Tags` 15. Click `Next: Advanced >` 16. Select the appropriate `Advanced` options 17. Click `Next: Review + create >` 18. Click `Create` **Remediate from Azure CLI** ``` az network bastion create --location --name --public-ip-address --resource-group --vnet-name --scale-units --sku Standard [--disable-copy-paste true|false] [--enable-ip-connect true|false] [--enable-tunneling true|false] ``` **Remediate from PowerShell** Create the appropriate `Virtual network` settings and `Public IP Address` settings. ``` $subnetName = AzureBastionSubnet $subnet = New-AzVirtualNetworkSubnetConfig -Name $subnetName -AddressPrefix $virtualNet = New-AzVirtualNetwork -Name -ResourceGroupName -Location -AddressPrefix -Subnet $subnet $publicip = New-AzPublicIpAddress -ResourceGroupName -Name -Location -AllocationMethod Dynamic -Sku Standard ``` Create the `Azure Bastion` service using the information within the created variables from above. ``` New-AzBastion -ResourceGroupName -Name -PublicIpAddress $publicip -VirtualNetwork $virtualNet -Sku Standard -ScaleUnit ```", + "AuditProcedure": "**Audit from Azure Portal** 1. Click on `Bastions` 2. Ensure there is at least one `Bastion` host listed under the `Name` column **Audit from Azure CLI** **Note:** The Azure CLI `network bastion` module is in `Preview` as of this writing ``` az network bastion list --subscription ``` Ensure the output of the above command is not empty. **Audit from PowerShell** Retrieve the `Bastion` host(s) information for a specific `Resource Group` ``` Get-AzBastion -ResourceGroupName ``` Ensure the output of the above command is not empty.", + "AdditionalInformation": "", + "References": "https://learn.microsoft.com/en-us/azure/bastion/bastion-overview#sku:https://learn.microsoft.com/en-us/powershell/module/az.network/get-azbastion?view=azps-9.2.0:https://learn.microsoft.com/en-us/cli/azure/network/bastion?view=azure-cli-latest", + "DefaultValue": "By default, the Azure Bastion service is not configured." + } + ] + }, + { + "Id": "8.5", + "Description": "Ensure Azure DDoS Network Protection is enabled on virtual networks", + "Checks": [], + "Attributes": [ + { + "Section": "8 Security Services", + "Profile": "Level 2", + "AssessmentStatus": "Automated", + "Description": "Azure DDoS Network Protection defends resources in virtual networks against distributed denial-of-service (DDoS) attacks. While an automated assessment procedure exists for this recommendation, the assessment status remains manual. Determining the appropriateness of enabling Azure DDoS Network Protection depends on the context and requirements of each organization and environment.", + "RationaleStatement": "Virtual networks and resources are protected against attacks, helping to ensure reliability and availability for critical workloads.", + "ImpactStatement": "Azure DDoS Network Protection incurs a significant fixed monthly charge, with additional charges if more than 100 public IP resources are protected. Careful consideration and analysis should be applied before enabling DDoS protection. Refer to https://azure.microsoft.com/en-us/pricing/details/ddos-protection for detailed pricing information.", + "RemediationProcedure": "**Remediate from Azure Portal** 1. Go to `Virtual networks`. 2. Click the name of a virtual network. 3. Under `Settings`, click `DDoS protection`. 4. Next to `DDoS Network Protection`, click `Enable`. 5. Provide a DDoS protection plan resource ID, or select a DDoS protection plan from the drop-down menu. 6. Click `Save`. 7. Repeat steps 1-6 for each virtual network requiring remediation. **Remediate from Azure CLI** For each virtual network requiring remediation, run the following command to enable DDoS protection: ``` az network vnet update --resource-group --name --ddos-protection true --ddos-protection-plan ```", + "AuditProcedure": "**Audit from Azure Portal** 1. Go to `Virtual networks`. 2. Click the name of a virtual network. 3. Under `Settings`, click `DDoS protection`. 4. Ensure `DDoS Network Protection` is set to `Enable`. 5. Repeat steps 1-4 for each virtual network. **Audit from Azure CLI** Run the following command to list virtual networks: ``` az network vnet list ``` For each virtual network, run the following command to get the DDoS protection setting: ``` az network vnet show --resource-group --name --query enableDdosProtection ``` Ensure `true` is returned. **Audit from Azure Policy** - **Policy ID:** a7aca53f-2ed4-4466-a25e-0b45ade68efd - **Name:** 'Azure DDoS Protection should be enabled'", + "AdditionalInformation": "", + "References": "https://learn.microsoft.com/en-us/azure/ddos-protection/ddos-protection-overview:https://learn.microsoft.com/en-us/azure/ddos-protection/manage-ddos-protection:https://azure.microsoft.com/en-us/pricing/details/ddos-protection:https://learn.microsoft.com/en-us/cli/azure/network/vnet", + "DefaultValue": "DDoS protection is disabled by default." + } + ] + }, + { + "Id": "9.1.1", + "Description": "Ensure soft delete for Azure File Shares is Enabled", + "Checks": [ + "storage_ensure_file_shares_soft_delete_is_enabled" + ], + "Attributes": [ + { + "Section": "9 Storage Services", + "SubSection": "9.1 Azure Files", + "Profile": "Level 1", + "AssessmentStatus": "Automated", + "Description": "Azure Files offers soft delete for file shares, allowing you to easily recover your data when it is mistakenly deleted by an application or another storage account user.", + "RationaleStatement": "Important data could be accidentally deleted or removed by a malicious actor. With soft delete enabled, the data is retained for the defined retention period before permanent deletion, allowing for recovery of the data.", + "ImpactStatement": "When a file share is soft-deleted, the used portion of the storage is charged for the indicated soft-deleted period. All other meters are not charged unless the share is restored.", + "RemediationProcedure": "**Remediate from Azure Portal** 1. Go to `Storage Accounts`. 1. For each storage account with file shares, under `Data storage`, click `File shares`. 1. Under `File share settings`, click the value next to `Soft delete`. 1. Under `Soft delete for all file shares`, click the toggle to set it to `Enabled`. 1. Under `Retention policies`, set an appropriate number of days to retain soft deleted data between 1 and 365, inclusive. 1. Click `Save`. **Remediate from Azure CLI** For each storage account requiring remediation, run the following command to enable soft delete for file shares and set an appropriate number of days for deleted data to be retained, between 1 and 365, inclusive: ``` az storage account file-service-properties update --account-name --enable-delete-retention true --delete-retention-days ``` **Remediate from PowerShell** For each storage account requiring remediation, run the following command to enable soft delete for file shares and set an appropriate number of days for deleted data to be retained, between 1 and 365, inclusive: ``` Update-AzStorageFileServiceProperty -ResourceGroupName -AccountName -EnableShareDeleteRetentionPolicy $true -ShareRetentionDays ```", + "AuditProcedure": "**Audit from Azure Portal** 1. Go to `Storage Accounts`. 1. For each storage account with file shares, under `Data storage`, click on `File shares`. 1. Under `File share settings`, ensure the value for `Soft delete` shows a number of days between 1 and 365, inclusive. **Audit from Azure CLI** Run the following command to list storage accounts: ``` az storage account list ``` Run the following command to determine if a storage account has file shares: ``` az storage share list --account-name ``` For each storage account with file shares, run the following command: ``` az storage account file-service-properties show --resource-group --account-name ``` Ensure that under `shareDeleteRetentionPolicy`, `enabled` is set to `true`, and `days` is set to an appropriate value between 1 and 365, inclusive. **Audit from PowerShell** Run the following command to list storage accounts: ``` Get-AzStorageAccount -ResourceGroupName ``` With a storage account context set, run the following command to determine if a storage account has file shares: ``` Get-AzStorageShare ``` For each storage account with file shares, run the following command: ``` Get-AzStorageFileServiceProperty -ResourceGroupName -AccountName ``` Ensure that `ShareDeleteRetentionPolicy.Enabled` is set to `True` and `ShareDeleteRetentionPolicy.Days` is set to an appropriate value between 1 and 365, inclusive.", + "AdditionalInformation": "", + "References": "https://learn.microsoft.com/en-us/azure/storage/files/storage-files-enable-soft-delete:https://learn.microsoft.com/en-us/cli/azure/storage/account/file-service-properties:https://learn.microsoft.com/en-us/powershell/module/az.storage/get-azstoragefileserviceproperty:https://learn.microsoft.com/en-us/powershell/module/az.storage/update-azstoragefileserviceproperty:https://learn.microsoft.com/en-us/azure/storage/files/storage-files-prevent-file-share-deletion", + "DefaultValue": "Soft delete is enabled by default at the storage account file share setting level." + } + ] + }, + { + "Id": "9.1.2", + "Description": "Ensure 'SMB protocol version' is set to 'SMB 3.1.1' or higher for SMB file shares", + "Checks": [ + "storage_smb_protocol_version_is_latest" + ], + "Attributes": [ + { + "Section": "9 Storage Services", + "SubSection": "9.1 Azure Files", + "Profile": "Level 1", + "AssessmentStatus": "Automated", + "Description": "Ensure that SMB file shares are configured to use the latest supported SMB protocol version. Keeping the SMB protocol updated helps mitigate risks associated with older SMB versions, which may contain vulnerabilities and lack essential security controls.", + "RationaleStatement": "Using the latest supported SMB protocol version enhances the security of SMB file shares by preventing the exploitation of known vulnerabilities in outdated SMB versions.", + "ImpactStatement": "Using the latest SMB protocol version may impact client compatibility.", + "RemediationProcedure": "**Remediate from Azure Portal** 1. Go to `Storage accounts`. 1. Click the name of a storage account. 1. Under `Data storage`, click `File shares`. 1. Under `File share settings`, click the link next to `Security`. 1. If `Profile` is set to `Maximum compatibility`, click the drop-down menu and select `Maximum security` or `Custom`. 1. If selecting `Custom`, under `SMB protocol versions`, uncheck the boxes next to `SMB 2.1` and `SMB 3.0`. 1. Click `Save`. 1. Repeat steps 1-7 for each storage account requiring remediation. **Remediate from Azure CLI** For each storage account requiring remediation, run the following command to set the SMB protocol version: ``` az storage account file-service-properties update --resource-group --account-name --versions SMB3.1.1 ``` **Remediate from PowerShell** For each storage account requiring remediation, run the following command to set the SMB protocol version: ``` Update-AzStorageFileServiceProperty -ResourceGroupName -StorageAccountName -SmbProtocolVersion SMB3.1.1 ```", + "AuditProcedure": "**Audit from Azure Portal** 1. Go to `Storage accounts`. 1. Click the name of a storage account. 1. Under `Data storage`, click `File shares`. 1. Under `File share settings`, click the link next to `Security`. 1. Under `SMB protocol versions`, ensure that `SMB3.1.1` is the only checked protocol version. 1. Repeat steps 1-5 for each storage account. **Audit from Azure CLI** Run the following command to list storage accounts: ``` az storage account list ``` For each storage account, run the following command: ``` az storage account file-service-properties show --resource-group --account-name ``` Ensure that under `protocolSettings` > `smb`, `versions` is set to `SMB3.1.1;` only. **Audit from PowerShell** Run the following command to list storage accounts: ``` Get-AzStorageAccount ``` Run the following command to get the file service properties for a storage account in a resource group with a given name: ``` $storageaccountfileservice = Get-AzStorageFileServiceProperty -ResourceGroupName -AccountName ``` Run the following command to get the SMB protocol version setting: ``` $storageaccountfileservice.ProtocolSettings.Smb.Versions ``` Ensure that the command returns `SMB3.1.1` only. Repeat for each storage account.", + "AdditionalInformation": "", + "References": "https://learn.microsoft.com/en-us/azure/well-architected/service-guides/azure-files#recommendations-for-smb-file-shares:https://learn.microsoft.com/en-us/azure/storage/files/files-smb-protocol#smb-security-settings:https://learn.microsoft.com/en-us/cli/azure/storage/account/file-service-properties:https://learn.microsoft.com/en-us/powershell/module/az.storage/get-azstoragefileserviceproperty:https://learn.microsoft.com/en-us/powershell/module/az.storage/update-azstoragefileserviceproperty", + "DefaultValue": "By default, all SMB versions are allowed." + } + ] + }, + { + "Id": "9.1.3", + "Description": "Ensure 'SMB channel encryption' is set to 'AES-256-GCM' or higher for SMB file shares", + "Checks": [ + "storage_smb_channel_encryption_with_secure_algorithm" + ], + "Attributes": [ + { + "Section": "9 Storage Services", + "SubSection": "9.1 Azure Files", + "Profile": "Level 1", + "AssessmentStatus": "Automated", + "Description": "Implement SMB channel encryption with AES-256-GCM for SMB file shares to ensure data confidentiality and integrity in transit. This method offers strong protection against eavesdropping and man-in-the-middle attacks, safeguarding sensitive information.", + "RationaleStatement": "AES-256-GCM encryption enhances the security of data transmitted over SMB channels by safeguarding it from unauthorized interception and tampering.", + "ImpactStatement": "Using the AES-256-GCM SMB channel encryption may impact client compatibility.", + "RemediationProcedure": "**Remediate from Azure Portal** 1. Go to `Storage accounts`. 1. Click the name of a storage account. 1. Under `Data storage`, click `File shares`. 1. Under `File share settings`, click the link next to `Security`. 1. If `Profile` is set to `Maximum compatibility`, click the drop-down menu and select `Maximum security` or `Custom`. 1. If selecting `Custom`, under `SMB channel encryption`, uncheck the boxes next to `AES-128-CCM` and `AES-128-GCM`. 1. Click `Save`. 1. Repeat steps 1-7 for each storage account requiring remediation. **Remediate from Azure CLI** For each storage account requiring remediation, run the following command to set the SMB channel encryption: ``` az storage account file-service-properties update --resource-group --account-name --channel-encryption AES-256-GCM ``` **Remediate from PowerShell** For each storage account requiring remediation, run the following command to set the SMB channel encryption: ``` Update-AzStorageFileServiceProperty -ResourceGroupName -StorageAccountName -SmbChannelEncryption AES-256-GCM ```", + "AuditProcedure": "**Audit from Azure Portal** 1. Go to `Storage accounts`. 1. Click the name of a storage account. 1. Under `Data storage`, click `File shares`. 1. Under `File share settings`, click the link next to `Security`. 1. Under `SMB channel encryption`, ensure that `AES-256-GCM`, or higher, is the only checked SMB channel encryption setting. 1. Repeat steps 1-5 for each storage account. **Audit from Azure CLI** Run the following command to list storage accounts: ``` az storage account list ``` For each storage account, run the following command: ``` az storage account file-service-properties show --resource-group --account-name ``` Ensure that under `protocolSettings` > `smb`, `channelEncryption` is set to `AES-256-GCM;`, or higher, only. **Audit from PowerShell** Run the following command to list storage accounts: ``` Get-AzStorageAccount ``` Run the following command to get the file service properties for a storage account in a resource group with a given name: ``` $storageaccountfileservice = Get-AzStorageFileServiceProperty -ResourceGroupName -AccountName ``` Run the following command to get the SMB channel encryption setting: ``` $storageaccountfileservice.ProtocolSettings.Smb.ChannelEncryption ``` Ensure that the command returns `AES-256-GCM`, or higher, only. Repeat for each storage account.", + "AdditionalInformation": "", + "References": "https://learn.microsoft.com/en-us/azure/well-architected/service-guides/azure-files#recommendations-for-smb-file-shares:https://learn.microsoft.com/en-us/azure/storage/files/files-smb-protocol?tabs=azure-portal#smb-security-settings:https://learn.microsoft.com/en-us/cli/azure/storage/account/file-service-properties:https://learn.microsoft.com/en-us/powershell/module/az.storage/get-azstoragefileserviceproperty:https://learn.microsoft.com/en-us/powershell/module/az.storage/update-azstoragefileserviceproperty", + "DefaultValue": "By default, the following SMB channel encryption algorithms are allowed: - AES-128-CCM - AES-128-GCM - AES-256-GCM" + } + ] + }, + { + "Id": "9.2.1", + "Description": "Ensure that soft delete for blobs on Azure Blob Storage storage accounts is Enabled", + "Checks": [ + "storage_ensure_soft_delete_is_enabled" + ], + "Attributes": [ + { + "Section": "9 Storage Services", + "SubSection": "9.2 Azure Blob Storage", + "Profile": "Level 1", + "AssessmentStatus": "Automated", + "Description": "Blobs in Azure storage accounts may contain sensitive or personal data, such as ePHI or financial information. Data that is erroneously modified or deleted by an application or a user can lead to data loss or unavailability. It is recommended that soft delete be enabled on Azure storage accounts with blob storage to allow for the preservation and recovery of data when blobs or blob snapshots are deleted.", + "RationaleStatement": "Blobs can be deleted incorrectly. An attacker or malicious user may do this deliberately in order to cause disruption. Deleting an Azure storage blob results in immediate data loss. Enabling this configuration for Azure storage accounts ensures that even if blobs are deleted from the storage account, the blobs are recoverable for a specific period of time, which is defined in the Retention policies, ranging from 7 to 365 days.", + "ImpactStatement": "All soft-deleted data is billed at the same rate as active data. Additional costs may be incurred for deleted blobs until the soft delete period ends and the data is permanently removed.", + "RemediationProcedure": "**Remediate from Azure Portal** 1. Go to `Storage Accounts`. 1. For each Storage Account with blob storage, under `Data management`, go to `Data protection`. 1. Check the box next to `Enable soft delete for blobs`. 1. Set the retention period to a sufficient length for your organization. 1. Click `Save`. **Remediate from Azure CLI** For each storage account requiring remediation, run the following command to enable soft delete for blobs: ``` az storage blob service-properties delete-policy update --days-retained --account-name --enable true ```", + "AuditProcedure": "**Audit from Azure Portal** 1. Go to `Storage Accounts`. 1. For each Storage Account with blob storage, under `Data management`, go to `Data protection`. 1. Ensure that `Enable soft delete for blobs` is checked. 1. Ensure that the retention period is a sufficient length for your organization. **Audit from Azure CLI** Run the following command to list storage accounts: ``` az storage account list ``` Run the following command to determine if a storage account has containers: ``` az storage container list --account-name ``` For each storage account with containers, ensure that the output of the below command contains `enabled: true` and `days` is not `null`: ``` az storage blob service-properties delete-policy show --account-name ```", + "AdditionalInformation": "", + "References": "https://learn.microsoft.com/en-us/azure/storage/blobs/soft-delete-blob-overview", + "DefaultValue": "Soft delete for blob storage is **enabled** by default on storage accounts created via the Azure Portal, and **disabled** by default on storage accounts created via Azure CLI or PowerShell." + } + ] + }, + { + "Id": "9.2.2", + "Description": "Ensure that soft delete for containers on Azure Blob Storage storage accounts is Enabled", + "Checks": [ + "storage_ensure_soft_delete_is_enabled" + ], + "Attributes": [ + { + "Section": "9 Storage Services", + "SubSection": "9.2 Azure Blob Storage", + "Profile": "Level 1", + "AssessmentStatus": "Automated", + "Description": "Blobs in Azure storage accounts may contain sensitive or personal data, such as ePHI or financial information. Data that is erroneously modified or deleted by an application or a user can lead to data loss or unavailability. It is recommended that soft delete be enabled on Azure storage accounts with blob storage to allow for the preservation and recovery of data when blobs or blob snapshots are deleted.", + "RationaleStatement": "Blobs can be deleted incorrectly. An attacker or malicious user may do this deliberately in order to cause disruption. Deleting an Azure storage blob results in immediate data loss. Enabling this configuration for Azure storage accounts ensures that even if blobs are deleted from the storage account, the blobs are recoverable for a specific period of time, which is defined in the Retention policies, ranging from 7 to 365 days.", + "ImpactStatement": "All soft-deleted data is billed at the same rate as active data. Additional costs may be incurred for deleted blobs until the soft delete period ends and the data is permanently removed.", + "RemediationProcedure": "**Remediate from Azure Portal** 1. Go to `Storage Accounts`. 1. For each Storage Account with blob storage, under `Data management`, go to `Data protection`. 1. Check the box next to `Enable soft delete for blobs`. 1. Set the retention period to a sufficient length for your organization. 1. Click `Save`. **Remediate from Azure CLI** For each storage account requiring remediation, run the following command to enable soft delete for blobs: ``` az storage blob service-properties delete-policy update --days-retained --account-name --enable true ```", + "AuditProcedure": "**Audit from Azure Portal** 1. Go to `Storage Accounts`. 1. For each Storage Account with blob storage, under `Data management`, go to `Data protection`. 1. Ensure that `Enable soft delete for blobs` is checked. 1. Ensure that the retention period is a sufficient length for your organization. **Audit from Azure CLI** Run the following command to list storage accounts: ``` az storage account list ``` Run the following command to determine if a storage account has containers: ``` az storage container list --account-name ``` For each storage account with containers, ensure that the output of the below command contains `enabled: true` and `days` is not `null`: ``` az storage blob service-properties delete-policy show --account-name ```", + "AdditionalInformation": "", + "References": "https://learn.microsoft.com/en-us/azure/storage/blobs/soft-delete-blob-overview", + "DefaultValue": "Soft delete for blob storage is **enabled** by default on storage accounts created via the Azure Portal, and **disabled** by default on storage accounts created via Azure CLI or PowerShell." + } + ] + }, + { + "Id": "9.2.3", + "Description": "Ensure 'Versioning' is set to 'Enabled' on Azure Blob Storage storage accounts", + "Checks": [ + "storage_blob_versioning_is_enabled" + ], + "Attributes": [ + { + "Section": "9 Storage Services", + "SubSection": "9.2 Azure Blob Storage", + "Profile": "Level 2", + "AssessmentStatus": "Automated", + "Description": "Enabling blob versioning allows for the automatic retention of previous versions of objects. With blob versioning enabled, earlier versions of a blob are accessible for data recovery in the event of modifications or deletions.", + "RationaleStatement": "Blob versioning safeguards data integrity and enables recovery by retaining previous versions of stored objects, facilitating quick restoration from accidental deletion, modification, or malicious activity.", + "ImpactStatement": "Enabling blob versioning for a storage account creates a new version with each write operation to a blob, which can increase storage costs. To control these costs, a lifecycle management policy can be applied to automatically delete older versions.", + "RemediationProcedure": "**Remediate from Azure Portal** 1. Go to `Storage accounts`. 1. Click the name of a storage account with blob storage. 1. In the `Overview` page, on the `Properties` tab, under `Blob service`, click `Disabled` next to `Versioning`. 1. Under `Tracking`, check the box next to `Enable versioning for blobs`. 1. Select the radio button next to `Keep all versions` or `Delete versions after (in days)`. 1. If selecting to delete versions, enter a number of in the box after which to delete blob versions. 1. Click `Save`. 1. Repeat steps 1-7 for each storage account with blob storage. **Remediate from Azure CLI** For each storage account requiring remediation, run the following command to enable blob versioning: ``` az storage account blob-service-properties update --account-name --enable-versioning true ``` **Remediate from PowerShell** For each storage account requiring remediation, run the following command to enable blob versioning: ``` Update-AzStorageBlobServiceProperty -ResourceGroupName -StorageAccountName -IsVersioningEnabled $true ```", + "AuditProcedure": "**Audit from Azure Portal** 1. Go to `Storage accounts`. 1. Click the name of a storage account with blob storage. 1. In the `Overview` page, on the `Properties` tab, under `Blob service`, ensure `Versioning` is set to `Enabled`. 1. Repeat steps 1-3 for each storage account with blob storage. **Audit from Azure CLI** Run the following command to list storage accounts: ``` az storage account list ``` Run the following command to determine if a storage account has containers: ``` az storage container list --account-name ``` For each storage account with containers, ensure that the output of the below command contains `isVersioningEnabled: true`: ``` az storage account blob-service-properties show --account-name ``` **Audit from PowerShell** Run the following command to list storage accounts: ``` Get-AzStorageAccount ``` Run the following command to create an Azure Storage context for a storage account: ``` $context = New-AzStorageContext -StorageAccountName ``` Run the following command to list containers for the storage account: ``` Get-AzStorageContainer -Context $context ``` If the storage account has containers, run the following command to get the blob service properties of the storage account: ``` $account = Get-AzStorageBlobServiceProperty -ResourceGroupName -AccountName ``` Run the following command to get the blob versioning setting for the storage account: ``` $account.IsVersioningEnabled ``` Ensure that the command returns `True`. Repeat for each storage account. **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:** [c36a325b-ae04-4863-ad4f-19c6678f8e08](https://portal.azure.com/#view/Microsoft_Azure_Policy/PolicyDetailBlade/definitionId/%2Fproviders%2FMicrosoft.Authorization%2FpolicyDefinitions%2Fc36a325b-ae04-4863-ad4f-19c6678f8e08) **- Name:** 'Configure your Storage account to enable blob versioning'", + "AdditionalInformation": "", + "References": "https://learn.microsoft.com/en-us/cli/azure/storage/account:https://learn.microsoft.com/en-us/cli/azure/storage/account/blob-service-properties:https://learn.microsoft.com/en-us/powershell/module/az.storage/get-azstorageaccount:https://learn.microsoft.com/en-us/powershell/module/az.storage/new-azstoragecontext:https://learn.microsoft.com/en-us/powershell/module/az.storage/get-azstoragecontainer:https://learn.microsoft.com/en-us/powershell/module/az.storage/get-azstorageblobserviceproperty:https://learn.microsoft.com/en-us/powershell/module/az.storage/update-azstorageblobserviceproperty:https://learn.microsoft.com/en-us/azure/storage/blobs/versioning-overview:https://learn.microsoft.com/en-us/azure/storage/blobs/lifecycle-management-overview", + "DefaultValue": "Blob versioning is disabled by default on storage accounts." + } + ] + }, + { + "Id": "9.3.1.1", + "Description": "Ensure that 'Enable key rotation reminders' is enabled for each Storage Account", + "Checks": [ + "storage_infrastructure_encryption_is_enabled" + ], + "Attributes": [ + { + "Section": "9 Storage Services", + "SubSection": "9.3 Storage Accounts", + "Profile": "Level 1", + "AssessmentStatus": "Automated", + "Description": "Access Keys authenticate application access requests to data contained in Storage Accounts. A periodic rotation of these keys is recommended to ensure that potentially compromised keys cannot result in a long-term exploitable credential. The Rotation Reminder is an automatic reminder feature for a manual procedure.", + "RationaleStatement": "Reminders such as those generated by this recommendation will help maintain a regular and healthy cadence for activities which improve the overall efficacy of a security program. Cryptographic key rotation periods will vary depending on your organization's security requirements and the type of data which is being stored in the Storage Account. For example, PCI DSS mandates that cryptographic keys be replaced or rotated 'regularly,' and advises that keys for static data stores be rotated every 'few months.' For the purposes of this recommendation, 90 days will be prescribed for the reminder. Review and adjustment of the 90 day period is recommended, and may even be necessary. Your organization's security requirements should dictate the appropriate setting.", + "ImpactStatement": "This recommendation only creates a periodic reminder to regenerate access keys. Regenerating access keys can affect services in Azure as well as the organization's applications that are dependent on the storage account. All clients that use the access key to access the storage account must be updated to use the new key.", + "RemediationProcedure": "**Remediate from Azure Portal** 1. Go to `Storage Accounts` 1. For each Storage Account that is not compliant, under `Security + networking`, go to `Access keys` 1. Click `Set rotation reminder` 1. Check `Enable key rotation reminders` 1. In the `Send reminders` field select `Custom`, then set the `Remind me every` field to `90` and the period drop down to `Days` 1. Click `Save` **Remediate from Powershell** ``` $rgName = $accountName = $account = Get-AzStorageAccount -ResourceGroupName $rgName -Name $accountName if ($account.KeyCreationTime.Key1 -eq $null -or $account.KeyCreationTime.Key2 -eq $null){ Write-output (You must regenerate both keys at least once before setting expiration policy) } else { $account = Set-AzStorageAccount -ResourceGroupName $rgName -Name $accountName -KeyExpirationPeriodInDay 90 } $account.KeyPolicy.KeyExpirationPeriodInDays ```", + "AuditProcedure": "**Audit from Azure Portal** 1. Go to `Storage Accounts` 2. For each Storage Account, under `Security + networking`, go to `Access keys` 3. If the button `Edit rotation reminder` is displayed, the Storage Account is compliant. Click `Edit rotation reminder` and review the `Remind me every` field for a desirable periodic setting that fits your security program's needs. If the button `Set rotation reminder` is displayed, the Storage Account is not compliant. **Audit from Powershell** ``` $rgName = $accountName = $account = Get-AzStorageAccount -ResourceGroupName $rgName -Name $accountName Write-Output $accountName -> Write-Output Expiration Reminder set to: $($account.KeyPolicy.KeyExpirationPeriodInDays) Days Write-Output Key1 Last Rotated: $($account.KeyCreationTime.Key1.ToShortDateString()) Write-Output Key2 Last Rotated: $($account.KeyCreationTime.Key2.ToShortDateString()) ``` Key rotation is recommended if the creation date for any key is empty. If the reminder is set, the period in days will be returned. The recommended period is 90 days. **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:** [044985bb-afe1-42cd-8a36-9d5d42424537](https://portal.azure.com/#view/Microsoft_Azure_Policy/PolicyDetailBlade/definitionId/%2Fproviders%2FMicrosoft.Authorization%2FpolicyDefinitions%2F044985bb-afe1-42cd-8a36-9d5d42424537) **- Name:** 'Storage account keys should not be expired'", + "AdditionalInformation": "", + "References": "https://docs.microsoft.com/en-us/azure/storage/common/storage-create-storage-account#regenerate-storage-access-keys: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-identity-management#im-3-manage-application-identities-securely-and-automatically: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-identity-management#im-8-restrict-the-exposure-of-credentials-and-secrets:https://www.pcidssguide.com/pci-dss-key-rotation-requirements/:https://nvlpubs.nist.gov/nistpubs/SpecialPublications/NIST.SP.800-57pt1r5.pdf", + "DefaultValue": "By default, Key rotation reminders are not configured." + } + ] + }, + { + "Id": "9.3.1.2", + "Description": "Ensure that Storage Account access keys are periodically regenerated", + "Checks": [ + "storage_key_rotation_90_days" + ], + "Attributes": [ + { + "Section": "9 Storage Services", + "SubSection": "9.3 Storage Accounts", + "Profile": "Level 1", + "AssessmentStatus": "Automated", + "Description": "For increased security, regenerate storage account access keys periodically.", + "RationaleStatement": "When a storage account is created, Azure generates two 512-bit storage access keys which are used for authentication when the storage account is accessed. Rotating these keys periodically ensures that any inadvertent access or exposure does not result from the compromise of these keys. Cryptographic key rotation periods will vary depending on your organization's security requirements and the type of data which is being stored in the Storage Account. For example, PCI DSS mandates that cryptographic keys be replaced or rotated 'regularly,' and advises that keys for static data stores be rotated every 'few months.' For the purposes of this recommendation, 90 days will be prescribed for the reminder. Review and adjustment of the 90 day period is recommended, and may even be necessary. Your organization's security requirements should dictate the appropriate setting.", + "ImpactStatement": "Regenerating access keys can affect services in Azure as well as the organization's applications that are dependent on the storage account. All clients who use the access key to access the storage account must be updated to use the new key.", + "RemediationProcedure": "**Remediate from Azure Portal** 1. Go to `Storage Accounts`. 2. For each Storage Account with outdated keys, under `Security + networking`, go to `Access keys`. 3. Click `Rotate key` next to the outdated key, then click `Yes` to the prompt confirming that you want to regenerate the access key. After Azure regenerates the Access Key, you can confirm that `Access keys` reflects a `Last rotated` date of `(0 days ago)`.", + "AuditProcedure": "**Audit from Azure Portal** 1. Go to `Storage Accounts`. 2. For each Storage Account, under `Security + networking`, go to `Access keys`. 3. Review the date and days in the `Last rotated` field for **each** key. If the `Last rotated` field indicates a number or days greater than 90 [or greater than your organization's period of validity], the key should be rotated. **Audit from Azure CLI** 1. Get a list of storage accounts ``` az storage account list --subscription ``` Make a note of `id`, `name` and `resourceGroup`. 2. For every storage account make sure that key is regenerated in the past 90 days. ``` az monitor activity-log list --namespace Microsoft.Storage --offset 90d --query [?contains(authorization.action, 'regenerateKey')] --resource-id ``` The output should contain ``` authorization/scope: AND authorization/action: Microsoft.Storage/storageAccounts/regeneratekey/action AND status/localizedValue: Succeeded status/Value: Succeeded ``` **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:** [044985bb-afe1-42cd-8a36-9d5d42424537](https://portal.azure.com/#view/Microsoft_Azure_Policy/PolicyDetailBlade/definitionId/%2Fproviders%2FMicrosoft.Authorization%2FpolicyDefinitions%2F044985bb-afe1-42cd-8a36-9d5d42424537) **- Name:** 'Storage account keys should not be expired'", + "AdditionalInformation": "", + "References": "https://docs.microsoft.com/en-us/azure/storage/common/storage-create-storage-account#regenerate-storage-access-keys: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-identity-management#im-2-protect-identity-and-authentication-systems:https://learn.microsoft.com/en-us/security/benchmark/azure/mcsb-governance-strategy#gs-6-define-and-implement-identity-and-privileged-access-strategy:https://www.pcidssguide.com/pci-dss-key-rotation-requirements/:https://nvlpubs.nist.gov/nistpubs/SpecialPublications/NIST.SP.800-57pt1r5.pdf", + "DefaultValue": "By default, access keys are not regenerated periodically." + } + ] + }, + { + "Id": "9.3.1.3", + "Description": "Ensure 'Allow storage account key access' for Azure Storage Accounts is 'Disabled'", + "Checks": [ + "storage_account_key_access_disabled" + ], + "Attributes": [ + { + "Section": "9 Storage Services", + "SubSection": "9.3 Storage Accounts", + "Profile": "Level 1", + "AssessmentStatus": "Automated", + "Description": "Every secure request to an Azure Storage account must be authorized. By default, requests can be authorized with either Microsoft Entra credentials or by using the account access key for Shared Key authorization.", + "RationaleStatement": "Microsoft Entra ID provides superior security and ease of use compared to Shared Key and is recommended by Microsoft. To require clients to use Microsoft Entra ID for authorizing requests, you can disallow requests to the storage account that are authorized with Shared Key.", + "ImpactStatement": "When you disallow Shared Key authorization for a storage account, any requests to the account that are authorized with Shared Key, including shared access signatures (SAS), will be denied. Client applications that currently access the storage account using the Shared Key will no longer function.", + "RemediationProcedure": "**Remediate from Azure Portal** 1. Go to `Storage accounts`. 1. Click on a storage account. 1. Under `Settings`, click `Configuration`. 1. Under `Allow storage account key access`, click the radio button next to `Disabled`. 1. Click `Save`. 1. Repeat steps 1-5 for each storage account requiring remediation. **Remediate from Azure CLI** For each storage account requiring remediation, run the following command to disallow shared key authorization: ``` az storage account update --resource-group --name --allow-shared-key-access false ``` **Remediate from PowerShell** For each storage account requiring remediation, run the following command to disallow shared key authorization: ``` Set-AzStorageAccount -ResourceGroupName -Name -AllowSharedKeyAccess $false ```", + "AuditProcedure": "**Audit from Azure Portal** 1. Go to `Storage accounts`. 1. Click on a storage account. 1. Under `Settings`, click `Configuration`. 1. Under `Allow storage account key access`, ensure that the radio button next to `Disabled` is selected. 1. Repeat steps 1-4 for each storage account. **Audit from Azure CLI** Run the following command to list storage accounts: ``` az storage account list ``` For each storage account, run the following command: ``` az storage account show --resource-group --name ``` Ensure that `allowSharedKeyAccess` is set to `false`. **Audit from PowerShell** Run the following command to list storage accounts: ``` Get-AzStorageAccount ``` Run the following command to get the storage account in a resource group with a given name: ``` $storageAccount = Get-AzStorageAccount -ResourceGroupName -Name ``` Run the following command to get the shared key access setting for the storage account: ``` $storageAccount.allowSharedKeyAccess ``` Ensure that the command returns `False`. Repeat for each storage account. **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:** [8c6a50c6-9ffd-4ae7-986f-5fa6111f9a54](https://portal.azure.com/#view/Microsoft_Azure_Policy/PolicyDetailBlade/definitionId/%2Fproviders%2FMicrosoft.Authorization%2FpolicyDefinitions%2F8c6a50c6-9ffd-4ae7-986f-5fa6111f9a54) **- Name:** 'Storage accounts should prevent shared key access'", + "AdditionalInformation": "", + "References": "https://learn.microsoft.com/en-us/azure/storage/common/shared-key-authorization-prevent:https://learn.microsoft.com/en-us/cli/azure/storage/account:https://learn.microsoft.com/en-us/powershell/module/az.storage/get-azstorageaccount:https://learn.microsoft.com/en-us/powershell/module/az.storage/set-azstorageaccount", + "DefaultValue": "The AllowSharedKeyAccess property of a storage account is not set by default and does not return a value until you explicitly set it. The storage account permits requests that are authorized with the Shared Key when the property value is **null** or when it is **true**." + } + ] + }, + { + "Id": "9.3.2.1", + "Description": "Ensure Private Endpoints are used to access Storage Accounts", + "Checks": [ + "storage_ensure_private_endpoints_in_storage_accounts" + ], + "Attributes": [ + { + "Section": "9 Storage Services", + "SubSection": "9.3 Storage Accounts", + "Profile": "Level 2", + "AssessmentStatus": "Automated", + "Description": "Use private endpoints for your Azure Storage accounts to allow clients and services to securely access data located over a network via an encrypted Private Link. To do this, the private endpoint uses an IP address from the VNet for each service. Network traffic between disparate services securely traverses encrypted over the VNet. This VNet can also link addressing space, extending your network and accessing resources on it. Similarly, it can be a tunnel through public networks to connect remote infrastructures together. This creates further security through segmenting network traffic and preventing outside sources from accessing it.", + "RationaleStatement": "Securing traffic between services through encryption protects the data from easy interception and reading.", + "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. Refer to https://azure.microsoft.com/en-us/pricing/details/private-link/ and https://azure.microsoft.com/en-us/pricing/calculator/ to estimate potential costs.", + "RemediationProcedure": "**Remediate from Azure Portal** 1. Open the `Storage Accounts` blade 1. For each listed Storage Account, perform the following: 1. Under the `Security + networking` heading, click on `Networking` 1. Click on the `Private endpoint connections` tab at the top of the networking window 1. Click the `+ Private endpoint` button 1. In the `1 - Basics` tab/step: - `Enter a name` that will be easily recognizable as associated with the Storage Account (*Note*: The Network Interface Name will be automatically completed, but you can customize it if needed.) - Ensure that the `Region` matches the region of the Storage Account - Click `Next` 1. In the `2 - Resource` tab/step: - Select the `target sub-resource` based on what type of storage resource is being made available - Click `Next` 1. In the `3 - Virtual Network` tab/step: - Select the `Virtual network` that your Storage Account will be connecting to - Select the `Subnet` that your Storage Account will be connecting to - (Optional) Select other network settings as appropriate for your environment - Click `Next` 1. In the `4 - DNS` tab/step: - (Optional) Select other DNS settings as appropriate for your environment - Click `Next` 1. In the `5 - Tags` tab/step: - (Optional) Set any tags that are relevant to your organization - Click `Next` 1. In the `6 - Review + create` tab/step: - A validation attempt will be made and after a few moments it should indicate `Validation Passed` - if it does not pass, double-check your settings before beginning more in depth troubleshooting. - If validation has passed, click `Create` then wait for a few minutes for the scripted deployment to complete. Repeat the above procedure for each Private Endpoint required within every Storage Account. **Remediate from PowerShell** ``` $storageAccount = Get-AzStorageAccount -ResourceGroupName '' -Name '' $privateEndpointConnection = @{ Name = 'connectionName' PrivateLinkServiceId = $storageAccount.Id GroupID = blob|blob_secondary|file|file_secondary|table|table_secondary|queue|queue_secondary|web|web_secondary|dfs|dfs_secondary } $privateLinkServiceConnection = New-AzPrivateLinkServiceConnection @privateEndpointConnection $virtualNetDetails = Get-AzVirtualNetwork -ResourceGroupName '' -Name '' $privateEndpoint = @{ ResourceGroupName = '' Name = '' Location = '' Subnet = $virtualNetDetails.Subnets[0] PrivateLinkServiceConnection = $privateLinkServiceConnection } New-AzPrivateEndpoint @privateEndpoint ``` **Remediate from Azure CLI** ``` az network private-endpoint create --resource-group --name --vnet-name --subnet --private-connection-resource-id --connection-name --group-id ```", + "AuditProcedure": "**Audit from Azure Portal** 1. Open the `Storage Accounts` blade. 1. For each listed Storage Account, perform the following check: 1. Under the `Security + networking` heading, click on `Networking`. 1. Click on the `Private endpoint connections` tab at the top of the networking window. 1. Ensure that for each VNet that the Storage Account must be accessed from, a unique Private Endpoint is deployed and the `Connection state` for each Private Endpoint is `Approved`. Repeat the procedure for each Storage Account. **Audit from PowerShell** ``` $storageAccount = Get-AzStorageAccount -ResourceGroup '' -Name '' Get-AzPrivateEndpoint -ResourceGroup ''|Where-Object {$_.PrivateLinkServiceConnectionsText -match $storageAccount.id} ``` If the results of the second command returns information, the Storage Account is using a Private Endpoint and complies with this Benchmark, otherwise if the results of the second command are empty, the Storage Account generates a finding. **Audit from Azure CLI** ``` az storage account show --name '' --query privateEndpointConnections[0].id ``` If the above command returns data, the Storage Account complies with this Benchmark, otherwise if the results are empty, the Storage Account generates a finding. **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:** [6edd7eda-6dd8-40f7-810d-67160c639cd9](https://portal.azure.com/#view/Microsoft_Azure_Policy/PolicyDetailBlade/definitionId/%2Fproviders%2FMicrosoft.Authorization%2FpolicyDefinitions%2F6edd7eda-6dd8-40f7-810d-67160c639cd9) **- Name:** 'Storage accounts should use private link'", + "AdditionalInformation": "A NAT gateway is the recommended solution for outbound internet access. This recommendation is based on the Common Reference Recommendation `Ensure Private Endpoints are used to access {service}`, from the `Common Reference Recommendations > Networking > Private Endpoints` section.", + "References": "https://docs.microsoft.com/en-us/azure/storage/common/storage-private-endpoints:https://docs.microsoft.com/en-us/azure/virtual-network/virtual-networks-overview:https://docs.microsoft.com/en-us/azure/private-link/create-private-endpoint-portal:https://docs.microsoft.com/en-us/azure/private-link/create-private-endpoint-cli?tabs=dynamic-ip:https://docs.microsoft.com/en-us/azure/private-link/create-private-endpoint-powershell?tabs=dynamic-ip:https://docs.microsoft.com/en-us/azure/private-link/tutorial-private-endpoint-storage-portal:https://learn.microsoft.com/en-us/security/benchmark/azure/mcsb-network-security#ns-2-secure-cloud-native-services-with-network-controls", + "DefaultValue": "By default, Private Endpoints are not created for Storage Accounts." + } + ] + }, + { + "Id": "9.3.2.2", + "Description": "Ensure that 'Public Network Access' is 'Disabled' for storage accounts", + "Checks": [ + "storage_blob_public_access_level_is_disabled" + ], + "Attributes": [ + { + "Section": "9 Storage Services", + "SubSection": "9.3 Storage Accounts", + "Profile": "Level 1", + "AssessmentStatus": "Automated", + "Description": "Disallowing public network access for a storage account overrides the public access settings for individual containers in that storage account for Azure Resource Manager Deployment Model storage accounts. Azure Storage accounts that use the classic deployment model will be retired on August 31, 2024.", + "RationaleStatement": "The default network configuration for a storage account permits a user with appropriate permissions to configure public network access to containers and blobs in a storage account. Keep in mind that public access to a container is always turned off by default and must be explicitly configured to permit anonymous requests. It grants read-only access to these resources without sharing the account key, and without requiring a shared access signature. It is recommended not to provide public network access to storage accounts until, and unless, it is strongly desired. A shared access signature token or Azure AD RBAC should be used for providing controlled and timed access to blob containers.", + "ImpactStatement": "Access will have to be managed using shared access signatures or via Azure AD RBAC. For classic storage accounts (to be retired on August 31, 2024), each container in the account must be configured to block anonymous access. Either configure all containers or to configure at the storage account level, migrate to the Azure Resource Manager deployment model.", + "RemediationProcedure": "**Remediate from Azure Portal** First, follow Microsoft documentation and create shared access signature tokens for your blob containers. Then, 1. Go to `Storage Accounts`. 1. For each storage account, under the `Security + networking` section, click `Networking`. 1. Set `Public network access` to `Disabled`. 1. Click `Save`. **Remediate from Azure CLI** Set 'Public Network Access' to `Disabled` on the storage account ``` az storage account update --name --resource-group --public-network-access Disabled ``` **Remediate from PowerShell** For each Storage Account, run the following to set the `PublicNetworkAccess` setting to `Disabled` ``` Set-AzStorageAccount -ResourceGroupName -Name -PublicNetworkAccess Disabled ```", + "AuditProcedure": "**Audit from Azure Portal** 1. Go to `Storage Accounts`. 2. For each storage account, under the `Security + networking` section, click `Networking`. 3. Ensure the `Public network access` setting is set to `Disabled`. **Audit from Azure CLI** Ensure `publicNetworkAccess` is `Disabled` ``` az storage account show --name --resource-group --query {publicNetworkAccess:publicNetworkAccess} ``` **Audit from PowerShell** For each Storage Account, ensure `PublicNetworkAccess` is `Disabled` ``` Get-AzStorageAccount -Name -ResourceGroupName |select PublicNetworkAccess ``` **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:** [b2982f36-99f2-4db5-8eff-283140c09693](https://portal.azure.com/#view/Microsoft_Azure_Policy/PolicyDetailBlade/definitionId/%2Fproviders%2FMicrosoft.Authorization%2FpolicyDefinitions%2Fb2982f36-99f2-4db5-8eff-283140c09693) **- Name:** 'Storage accounts should disable public network access'", + "AdditionalInformation": "This recommendation is based on the Common Reference Recommendation `Ensure public network access is Disabled`, from the `Common Reference Recommendations > Networking > Virtual Networks (VNets)` section.", + "References": "https://docs.microsoft.com/en-us/azure/storage/blobs/storage-manage-access-to-resources: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-network-security#ns-2-secure-cloud-native-services-with-network-controls:https://docs.microsoft.com/en-us/azure/storage/blobs/assign-azure-role-data-access:https://learn.microsoft.com/en-us/azure/storage/common/storage-network-security?tabs=azure-portal", + "DefaultValue": "By default, `Public Network Access` is set to `Enabled from all networks` for the Storage Account." + } + ] + }, + { + "Id": "9.3.2.3", + "Description": "Ensure default network access rule for storage accounts is set to deny", + "Checks": [ + "storage_default_network_access_rule_is_denied" + ], + "Attributes": [ + { + "Section": "9 Storage Services", + "SubSection": "9.3 Storage Accounts", + "Profile": "Level 1", + "AssessmentStatus": "Automated", + "Description": "Restricting default network access helps to provide a new layer of security, since storage accounts accept connections from clients on any network. To limit access to selected networks, the default action must be changed.", + "RationaleStatement": "Storage accounts should be configured to deny access to traffic from all networks (including internet traffic). Access can be granted to traffic from specific Azure Virtual networks, allowing a secure network boundary for specific applications to be built. Access can also be granted to public internet IP address ranges to enable connections from specific internet or on-premises clients. When network rules are configured, only applications from allowed networks can access a storage account. When calling from an allowed network, applications continue to require proper authorization (a valid access key or SAS token) to access the storage account.", + "ImpactStatement": "All allowed networks will need to be whitelisted on each specific network, creating administrative overhead. This may result in loss of network connectivity, so do not turn on for critical resources during business hours.", + "RemediationProcedure": "**Remediate from Azure Portal** 1. Go to `Storage Accounts`. 1. For each storage account, under `Security + networking`, click `Networking`. 1. Click the `Firewalls and virtual networks` heading. 1. Set `Public network access` to `Enabled from selected virtual networks and IP addresses`. 1. Add rules to allow traffic from specific networks and IP addresses. 1. Click `Save`. **Remediate from Azure CLI** Use the below command to update `default-action` to `Deny`. ``` az storage account update --name --resource-group --default-action Deny ```", + "AuditProcedure": "**Audit from Azure Portal** 1. Go to Storage Accounts. 2. For each storage account, under `Security + networking`, click `Networking`. 4. Click the `Firewalls and virtual networks` heading. 3. Ensure that `Public network access` is not set to `Enabled from all networks`. **Audit from Azure CLI** Ensure `defaultAction` is not set to ` Allow`. ``` az storage account list --query '[*].networkRuleSet' ``` **Audit from PowerShell** ``` Connect-AzAccount Set-AzContext -Subscription Get-AzStorageAccountNetworkRuleset -ResourceGroupName -Name |Select-Object DefaultAction ``` PowerShell Result - Non-Compliant ``` DefaultAction : Allow ``` PowerShell Result - Compliant ``` DefaultAction : Deny ``` **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:** [34c877ad-507e-4c82-993e-3452a6e0ad3c](https://portal.azure.com/#view/Microsoft_Azure_Policy/PolicyDetailBlade/definitionId/%2Fproviders%2FMicrosoft.Authorization%2FpolicyDefinitions%2F34c877ad-507e-4c82-993e-3452a6e0ad3c) **- Name:** 'Storage accounts should restrict network access' - **Policy ID:** [2a1a9cdf-e04d-429a-8416-3bfb72a1b26f](https://portal.azure.com/#view/Microsoft_Azure_Policy/PolicyDetailBlade/definitionId/%2Fproviders%2FMicrosoft.Authorization%2FpolicyDefinitions%2F2a1a9cdf-e04d-429a-8416-3bfb72a1b26f) **- Name:** 'Storage accounts should restrict network access using virtual network rules'", + "AdditionalInformation": "This recommendation is based on the Common Reference Recommendation `Ensure Network Access Rules are set to Deny-by-default`, from the `Common Reference Recommendations > Networking > Virtual Networks (VNets)` section.", + "References": "https://docs.microsoft.com/en-us/azure/storage/common/storage-network-security: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-network-security#ns-2-secure-cloud-native-services-with-network-controls", + "DefaultValue": "By default, Storage Accounts will accept connections from clients on any network." + } + ] + }, + { + "Id": "9.3.3.1", + "Description": "Ensure that 'Default to Microsoft Entra authorization in the Azure portal' is set to 'Enabled'", + "Checks": [ + "storage_default_to_entra_authorization_enabled" + ], + "Attributes": [ + { + "Section": "9 Storage Services", + "SubSection": "9.3 Storage Accounts", + "Profile": "Level 1", + "AssessmentStatus": "Automated", + "Description": "When this property is enabled, the Azure portal authorizes requests to blobs, files, queues, and tables with Microsoft Entra ID by default.", + "RationaleStatement": "Microsoft Entra ID provides superior security and ease of use over Shared Key.", + "ImpactStatement": "Users will need appropriate RBAC permissions to access storage data.", + "RemediationProcedure": "**Remediate from Azure Portal** 1. Go to `Storage accounts`. 1. Click the name of a storage account. 1. Under `Settings`, click `Configuration`. 1. Under `Default to Microsoft Entra authorization in the Azure portal`, click the radio button next to `Enabled`. 1. Click `Save`. 1. Repeat steps 1-5 for each storage account requiring remediation. **Remediate from Azure CLI** For each storage account requiring remediation, run the following command to enable `defaultToOAuthAuthentication`: ``` az storage account update --resource-group --name --set defaultToOAuthAuthentication=true ```", + "AuditProcedure": "**Audit from Azure Portal** 1. Go to `Storage accounts`. 1. Click the name of a storage account. 1. Under `Settings`, click `Configuration`. 1. Ensure that `Default to Microsoft Entra authorization in the Azure portal` is set to `Enabled`. 1. Repeat steps 1-4 for each storage account. **Audit from Azure CLI** Run the following command to get the `name` and `defaultToOAuthAuthentication` setting for each storage account: ``` az storage account list --query [*].[name,defaultToOAuthAuthentication] ``` Ensure that `true` is returned for each storage account.", + "AdditionalInformation": "", + "References": "https://learn.microsoft.com/en-us/azure/storage/blobs/authorize-data-operations-portal#default-to-microsoft-entra-authorization-in-the-azure-portal:https://learn.microsoft.com/en-us/cli/azure/storage/account?view=azure-cli-latest", + "DefaultValue": "By default, `defaultToOAuthAuthentication` is disabled." + } + ] + }, + { + "Id": "9.3.4", + "Description": "Ensure that 'Secure transfer required' is set to 'Enabled'", + "Checks": [ + "storage_secure_transfer_required_is_enabled" + ], + "Attributes": [ + { + "Section": "9 Storage Services", + "SubSection": "9.3 Storage Accounts", + "Profile": "Level 1", + "AssessmentStatus": "Automated", + "Description": "Enable data encryption in transit.", + "RationaleStatement": "The secure transfer option enhances the security of a storage account by only allowing requests to the storage account by a secure connection. For example, when calling REST APIs to access storage accounts, the connection must use HTTPS. Any requests using HTTP will be rejected when 'secure transfer required' is enabled. When using the Azure files service, connection without encryption will fail, including scenarios using SMB 2.1, SMB 3.0 without encryption, and some flavors of the Linux SMB client. Because Azure storage doesnt support HTTPS for custom domain names, this option is not applied when using a custom domain name.", + "ImpactStatement": "Applications using HTTP will need to be updated to use HTTPS.", + "RemediationProcedure": "**Remediate from Azure Portal** 1. Go to `Storage Accounts`. 1. For each storage account, under `Settings`, click `Configuration`. 1. Set `Secure transfer required` to `Enabled`. 1. Click `Save`. **Remediate from Azure CLI** Use the below command to enable `Secure transfer required` for a `Storage Account` ``` az storage account update --name --resource-group --https-only true ```", + "AuditProcedure": "**Audit from Azure Portal** 1. Go to `Storage Accounts`. 1. For each storage account, under `Settings`, click `Configuration`. 1. Ensure that `Secure transfer required` is set to `Enabled`. **Audit from Azure CLI** Use the below command to ensure the `Secure transfer required` is enabled for all the `Storage Accounts` by ensuring the output contains `true` for each of the `Storage Accounts`. ``` az storage account list --query [*].[name,enableHttpsTrafficOnly] ``` **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:** [404c3081-a854-4457-ae30-26a93ef643f9](https://portal.azure.com/#view/Microsoft_Azure_Policy/PolicyDetailBlade/definitionId/%2Fproviders%2FMicrosoft.Authorization%2FpolicyDefinitions%2F404c3081-a854-4457-ae30-26a93ef643f9) **- Name:** 'Secure transfer to storage accounts should be enabled'", + "AdditionalInformation": "", + "References": "https://docs.microsoft.com/en-us/azure/storage/blobs/security-recommendations#encryption-in-transit:https://docs.microsoft.com/en-us/cli/azure/storage/account?view=azure-cli-latest#az_storage_account_list:https://docs.microsoft.com/en-us/cli/azure/storage/account?view=azure-cli-latest#az_storage_account_update:https://learn.microsoft.com/en-us/security/benchmark/azure/mcsb-data-protection#dp-3-encrypt-sensitive-data-in-transit", + "DefaultValue": "By default, `Secure transfer required` is set to `Disabled`." + } + ] + }, + { + "Id": "9.3.5", + "Description": "Ensure 'Allow Azure services on the trusted services list to access this storage account' is Enabled for Storage Account Access", + "Checks": [ + "storage_ensure_azure_services_are_trusted_to_access_is_enabled" + ], + "Attributes": [ + { + "Section": "9 Storage Services", + "SubSection": "9.3 Storage Accounts", + "Profile": "Level 1", + "AssessmentStatus": "Automated", + "Description": "_NOTE:_ This recommendation assumes that the `Public network access` parameter is set to `Enabled from selected virtual networks and IP addresses`. Please ensure the prerequisite recommendation has been implemented before proceeding: - Ensure Default Network Access Rule for Storage Accounts is Set to Deny Some Azure services that interact with storage accounts operate from networks that can't be granted access through network rules. To help this type of service work as intended, allow the set of trusted Azure services to bypass the network rules. These services will then use strong authentication to access the storage account. If the `Allow Azure services on the trusted services list to access this storage account` exception is enabled, the following services are granted access to the storage account: Azure Backup, Azure Data Box, Azure DevTest Labs, Azure Event Grid, Azure Event Hubs, Azure File Sync, Azure HDInsight, Azure Import/Export, Azure Monitor, Azure Networking Services, and Azure Site Recovery (when registered in the subscription).", + "RationaleStatement": "Turning on firewall rules for a storage account will block access to incoming requests for data, including from other Azure services. We can re-enable this functionality by allowing access to `trusted Azure services` through networking exceptions.", + "ImpactStatement": "This creates authentication credentials for services that need access to storage resources so that services will no longer need to communicate via network request. There may be a temporary loss of communication as you set each Storage Account. It is recommended to not do this on mission-critical resources during business hours.", + "RemediationProcedure": "**Remediate from Azure Portal** 1. Go to `Storage Accounts`. 1. For each storage account, under `Security + networking`, click `Networking`. 1. Click on the `Firewalls and virtual networks` heading. 1. Under `Exceptions`, check the box next to `Allow Azure services on the trusted services list to access this storage account`. 1. Click `Save`. **Remediate from Azure CLI** Use the below command to update `bypass` to `Azure services`. ``` az storage account update --name --resource-group --bypass AzureServices ```", + "AuditProcedure": "**Audit from Azure Portal** 1. Go to `Storage Accounts`. 1. For each storage account, under `Security + networking`, click `Networking`. 1. Click on the `Firewalls and virtual networks` heading. 1. Under `Exceptions`, ensure that `Allow Azure services on the trusted services list to access this storage account` is checked. **Audit from Azure CLI** Ensure `bypass` contains `AzureServices` ``` az storage account list --query '[*].networkRuleSet' ``` **Audit from PowerShell** ``` Connect-AzAccount Set-AzContext -Subscription Get-AzStorageAccountNetworkRuleset -ResourceGroupName -Name |Select-Object Bypass ``` If the response from the above command is `None`, the storage account configuration is out of compliance with this check. If the response is `AzureServices`, the storage account configuration is in compliance with this check. **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:** [c9d007d0-c057-4772-b18c-01e546713bcd](https://portal.azure.com/#view/Microsoft_Azure_Policy/PolicyDetailBlade/definitionId/%2Fproviders%2FMicrosoft.Authorization%2FpolicyDefinitions%2Fc9d007d0-c057-4772-b18c-01e546713bcd) **- Name:** 'Storage accounts should allow access from trusted Microsoft services'", + "AdditionalInformation": "", + "References": "https://docs.microsoft.com/en-us/azure/storage/common/storage-network-security:https://learn.microsoft.com/en-us/security/benchmark/azure/mcsb-network-security#ns-2-secure-cloud-native-services-with-network-controls", + "DefaultValue": "By default, Storage Accounts will accept connections from clients on any network." + } + ] + }, + { + "Id": "9.3.6", + "Description": "Ensure the 'Minimum TLS version' for storage accounts is set to 'Version 1.2'", + "Checks": [ + "storage_ensure_minimum_tls_version_12" + ], + "Attributes": [ + { + "Section": "9 Storage Services", + "SubSection": "9.3 Storage Accounts", + "Profile": "Level 1", + "AssessmentStatus": "Automated", + "Description": "In some cases, Azure Storage sets the minimum TLS version to be version 1.0 by default. TLS 1.0 is a legacy version and has known vulnerabilities. This minimum TLS version can be configured to be later protocols such as TLS 1.2.", + "RationaleStatement": "TLS 1.0 has known vulnerabilities and has been replaced by later versions of the TLS protocol. Continued use of this legacy protocol affects the security of data in transit.", + "ImpactStatement": "When set to TLS 1.2 all requests must leverage this version of the protocol. Applications leveraging legacy versions of the protocol will fail.", + "RemediationProcedure": "**Remediate from Azure Portal** 1. Go to `Storage Accounts`. 1. For each storage account, under `Settings`, click `Configuration`. 1. Set the `Minimum TLS version` to `Version 1.2`. 1. Click `Save`. **Remediate from Azure CLI** ``` az storage account update \\ --name \\ --resource-group \\ --min-tls-version TLS1_2 ``` **Remediate from PowerShell** To set the minimum TLS version, run the following command: ``` Set-AzStorageAccount -AccountName ` -ResourceGroupName ` -MinimumTlsVersion TLS1_2 ```", + "AuditProcedure": "**Audit from Azure Portal** 1. Go to `Storage Accounts`. 1. For each storage account, under `Settings`, click `Configuration`. 1. Ensure that the `Minimum TLS version` is set to `Version 1.2`. **Audit from Azure CLI** Get a list of all storage accounts and their resource groups ``` az storage account list | jq '.[] | {name, resourceGroup}' ``` Then query the minimumTLSVersion field ``` az storage account show \\ --name \\ --resource-group \\ --query minimumTlsVersion \\ --output tsv ``` **Audit from PowerShell** To get the minimum TLS version, run the following command: ``` (Get-AzStorageAccount -Name -ResourceGroupName ).MinimumTlsVersion ``` **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:** [fe83a0eb-a853-422d-aac2-1bffd182c5d0](https://portal.azure.com/#view/Microsoft_Azure_Policy/PolicyDetailBlade/definitionId/%2Fproviders%2FMicrosoft.Authorization%2FpolicyDefinitions%2Ffe83a0eb-a853-422d-aac2-1bffd182c5d0) **- Name:** 'Storage accounts should have the specified minimum TLS version'", + "AdditionalInformation": "", + "References": "https://docs.microsoft.com/en-us/azure/storage/common/transport-layer-security-configure-minimum-version?tabs=portal:https://learn.microsoft.com/en-us/security/benchmark/azure/mcsb-data-protection#dp-3-encrypt-sensitive-data-in-transit", + "DefaultValue": "If a storage account is created through the portal, the MinimumTlsVersion property for that storage account will be set to TLS 1.2. If a storage account is created through PowerShell or CLI, the MinimumTlsVersion property for that storage account will not be set, and defaults to TLS 1.0." + } + ] + }, + { + "Id": "9.3.7", + "Description": "Ensure 'Cross Tenant Replication' is not enabled", + "Checks": [ + "storage_cross_tenant_replication_disabled" + ], + "Attributes": [ + { + "Section": "9 Storage Services", + "SubSection": "9.3 Storage Accounts", + "Profile": "Level 1", + "AssessmentStatus": "Automated", + "Description": "Cross Tenant Replication in Azure allows data to be replicated across multiple Azure tenants. While this feature can be beneficial for data sharing and availability, it also poses a significant security risk if not properly managed. Unauthorized data access, data leakage, and compliance violations are potential risks. Disabling Cross Tenant Replication ensures that data is not inadvertently replicated across different tenant boundaries without explicit authorization.", + "RationaleStatement": "Disabling Cross Tenant Replication minimizes the risk of unauthorized data access and ensures that data governance policies are strictly adhered to. This control is especially critical for organizations with stringent data security and privacy requirements, as it prevents the accidental sharing of sensitive information.", + "ImpactStatement": "Disabling Cross Tenant Replication may affect data availability and sharing across different Azure tenants. Ensure that this change aligns with your organizational data sharing and availability requirements.", + "RemediationProcedure": "**Remediate from Azure Portal** 1. Go to `Storage Accounts`. 1. For each storage account, under `Data management`, click `Object replication`. 1. Click `Advanced settings`. 1. Uncheck `Allow cross-tenant replication`. 1. Click `OK`. **Remediate from Azure CLI** Replace the information within <> with appropriate values: ``` az storage account update --name --resource-group --allow-cross-tenant-replication false ```", + "AuditProcedure": "**Audit from Azure Portal** 1. Go to `Storage Accounts`. 1. For each storage account, under `Data management`, click `Object replication`. 1. Click `Advanced settings`. 1. Ensure `Allow cross-tenant replication` is not checked. **Audit from Azure CLI** ``` az storage account list --query [*].[name,allowCrossTenantReplication] ``` The value of `false` should be returned for each storage account listed. **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:** [92a89a79-6c52-4a7e-a03f-61306fc49312](https://portal.azure.com/#view/Microsoft_Azure_Policy/PolicyDetailBlade/definitionId/%2Fproviders%2FMicrosoft.Authorization%2FpolicyDefinitions%2F92a89a79-6c52-4a7e-a03f-61306fc49312) **- Name:** 'Storage accounts should prevent cross tenant object replication'", + "AdditionalInformation": "", + "References": "https://learn.microsoft.com/en-us/azure/storage/blobs/object-replication-prevent-cross-tenant-policies?tabs=portal", + "DefaultValue": "For new storage accounts created after Dec 15, 2023 cross tenant replication is not enabled." + } + ] + }, + { + "Id": "9.3.8", + "Description": "Ensure that 'Allow Blob Anonymous Access' is set to 'Disabled'", + "Checks": [ + "storage_blob_public_access_level_is_disabled" + ], + "Attributes": [ + { + "Section": "9 Storage Services", + "SubSection": "9.3 Storage Accounts", + "Profile": "Level 1", + "AssessmentStatus": "Automated", + "Description": "The Azure Storage setting ‘Allow Blob Anonymous Access (aka allowBlobPublicAccess) controls whether anonymous access is allowed for blob data in a storage account. When this property is set to True, it enables public read access to blob data, which can be convenient for sharing data but may carry security risks. When set to False, it disallows public access to blob data, providing a more secure storage environment.", + "RationaleStatement": "If Allow Blob Anonymous Access is enabled, blobs can be accessed by adding the blob name to the URL to see the contents. An attacker can enumerate a blob using methods, such as brute force, and access them. Exfiltration of data by brute force enumeration of items from a storage account may occur if this setting is set to 'Enabled'.", + "ImpactStatement": "Additional consideration may be required for exceptional circumstances where elements of a storage account require public accessibility. In these circumstances, it is highly recommended that all data stored in the public facing storage account be reviewed for sensitive or potentially compromising data, and that sensitive or compromising data is never stored in these storage accounts.", + "RemediationProcedure": "**Remediate from Azure Portal** 1. Go to `Storage Accounts`. 1. For each storage account, under `Settings`, click `Configuration`. 1. Set `Allow Blob Anonymous Access` to `Disabled`. 1. Click `Save`. **Remediate from Powershell** For every storage account in scope, run the following: ``` $storageAccount = Get-AzStorageAccount -ResourceGroupName -Name $storageAccount.AllowBlobPublicAccess = $false Set-AzStorageAccount -InputObject $storageAccount ```", + "AuditProcedure": "**Audit from Azure Portal** 1. Go to `Storage Accounts`. 1. For each storage account, under `Settings`, click `Configuration`. 1. Ensure `Allow Blob Anonymous Access` is set to `Disabled`. **Audit from Azure CLI** For every storage account in scope: ``` az storage account show --name --query allowBlobPublicAccess ``` Ensure that every storage account in scope returns `false` for the allowBlobPublicAccess setting. **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:** [4fa4b6c0-31ca-4c0d-b10d-24b96f62a751](https://portal.azure.com/#view/Microsoft_Azure_Policy/PolicyDetailBlade/definitionId/%2Fproviders%2FMicrosoft.Authorization%2FpolicyDefinitions%2F4fa4b6c0-31ca-4c0d-b10d-24b96f62a751) **- Name:** 'Storage account public access should be disallowed'", + "AdditionalInformation": "Azure Storage accounts that use the classic deployment model will be retired on August 31, 2024.", + "References": "https://learn.microsoft.com/en-us/azure/storage/blobs/anonymous-read-access-prevent?tabs=portal:https://learn.microsoft.com/en-us/azure/storage/blobs/anonymous-read-access-prevent?source=recommendations&tabs=portal:Classic Storage Accounts: https://learn.microsoft.com/en-us/azure/storage/blobs/anonymous-read-access-prevent-classic?tabs=portal", + "DefaultValue": "Disabled" + } + ] + }, + { + "Id": "9.3.9", + "Description": "Ensure Azure Resource Manager Delete locks are applied to Azure Storage Accounts", + "Checks": [], + "Attributes": [ + { + "Section": "9 Storage Services", + "SubSection": "9.3 Storage Accounts", + "Profile": "Level 1", + "AssessmentStatus": "Manual", + "Description": "Azure Resource Manager _CannotDelete (Delete)_ locks can prevent users from accidentally or maliciously deleting a storage account. This feature ensures that while the Storage account can still be modified or used, deletion of the Storage account resource requires removal of the lock by a user with appropriate permissions. This feature is a protective control for the availability of data. By ensuring that a storage account or its parent resource group cannot be deleted without first removing the lock, the risk of data loss is reduced.", + "RationaleStatement": "Applying a _Delete_ lock on storage accounts protects the availability of data by preventing the accidental or unauthorized deletion of the entire storage account. It is a fundamental protective control that can prevent data loss", + "ImpactStatement": "- Prevents the deletion of the Storage account Resource entirely. - Prevents the deletion of the parent Resource Group containing the locked Storage account resource. - Does not prevent other control plane operations, including modification of configurations, network settings, containers, and access. - Does not prevent deletion of containers or other objects within the storage account.", + "RemediationProcedure": "**Remediate from Azure Portal** 1. Navigate to the storage account in the Azure portal. 1. Under the `Settings` section, select `Locks`. 1. Select `Add`. 1. Provide a Name, and choose `Delete` for the type of lock. 1. Add a note about the lock if desired. **Remediate from Azure CLI** Replace the information within <> with appropriate values: ``` az lock create --name \\ --resource-group \\ --resource \\ --lock-type CanNotDelete \\ --resource-type Microsoft.Storage/storageAccounts ``` **Remediate from PowerShell** Replace the information within <> with appropriate values: ``` New-AzResourceLock -LockLevel CanNotDelete ` -LockName ` -ResourceName ` -ResourceType Microsoft.Storage/storageAccounts ` -ResourceGroupName ```", + "AuditProcedure": "**Audit from Azure Portal** 1. Navigate to the storage account in the Azure portal. 1. For each storage account, under `Settings`, click `Locks`. 1. Ensure that a `Delete` lock exists on the storage account. **Audit from Azure CLI** ``` az lock list --resource-group \\ --resource-name \\ --resource-type Microsoft.Storage/storageAccounts ``` **Audit from PowerShell** ``` Get-AzResourceLock -ResourceGroupName ` -ResourceName ` -ResourceType Microsoft.Storage/storageAccounts ``` **Audit from Azure Policy** There is currently no built-in Microsoft policy to audit resource locks on storage accounts. Custom and community policy definitions can check for the existence of a “Microsoft.Authorization/locks” resource with an AuditIfNotExists effect.", + "AdditionalInformation": "", + "References": "https://learn.microsoft.com/en-us/azure/storage/common/lock-account-resource:https://learn.microsoft.com/en-us/azure/azure-resource-manager/management/lock-resources", + "DefaultValue": "By default, no locks are applied to Azure resources, including storage accounts. Locks must be manually configured after resource creation." + } + ] + }, + { + "Id": "9.3.10", + "Description": "Ensure Azure Resource Manager ReadOnly locks are considered for Azure Storage Accounts", + "Checks": [], + "Attributes": [ + { + "Section": "9 Storage Services", + "SubSection": "9.3 Storage Accounts", + "Profile": "Level 2", + "AssessmentStatus": "Manual", + "Description": "Adding an Azure Resource Manager `ReadOnly` lock can prevent users from accidentally or maliciously deleting a storage account, modifying its properties and containers, or creating access assignments. The lock must be removed before the storage account can be deleted or updated. It provides more protection than a `CannotDelete`-type of resource manager lock. This feature prevents `POST` operations on a storage account and containers to the Azure Resource Manager control plane, _management.azure.com_. Blocked operations include `listKeys` which prevents clients from obtaining the account shared access keys. Microsoft does not recommend `ReadOnly` locks for storage accounts with Azure Files and Table service containers. This Azure Resource Manager REST API documentation (spec) provides information about the control plane `POST` operations for _Microsoft.Storage_ resources.", + "RationaleStatement": "Applying a `ReadOnly` lock on storage accounts protects the confidentiality and availability of data by preventing the accidental or unauthorized deletion of the entire storage account and modification of the account, container properties, or access permissions. It can offer enhanced protection for blob and queue workloads with tradeoffs in usability and compatibility for clients using account shared access keys.", + "ImpactStatement": "- Prevents the deletion of the Storage account Resource entirely. - Prevents the deletion of the parent Resource Group containing the locked Storage account resource. - Prevents clients from obtaining the storage account shared access keys using a `listKeys` operation. - Requires Entra credentials to access blob and queue data in the Portal. - Data in Azure Files or the Table service may be inaccessible to clients using the account shared access keys. - Prevents modification of account properties, network settings, containers, and RBAC assignments. - Does not prevent access using existing account shared access keys issued to clients. - Does not prevent deletion of containers or other objects within the storage account.", + "RemediationProcedure": "**Remediate from Azure Portal** 1. Navigate to the storage account in the Azure portal. 1. Under the `Settings` section, select `Locks`. 1. Select `Add`. 1. Provide a Name, and choose `ReadOnly` for the type of lock. 1. Add a note about the lock if desired. **Remediate from Azure CLI** Replace the information within <> with appropriate values: ``` az lock create --name \\ --resource-group \\ --resource \\ --lock-type ReadOnly \\ --resource-type Microsoft.Storage/storageAccounts ``` **Remediate from PowerShell** Replace the information within <> with appropriate values: ``` New-AzResourceLock -LockLevel ReadOnly ` -LockName ` -ResourceName ` -ResourceType Microsoft.Storage/storageAccounts ` -ResourceGroupName ```", + "AuditProcedure": "**Audit from Azure Portal** 1. Navigate to the storage account in the Azure portal. 1. For each storage account, under `Settings`, click `Locks`. 1. Ensure that a `ReadOnly` lock exists on the storage account. **Audit from Azure CLI** ``` az lock list --resource-group \\ --resource-name \\ --resource-type Microsoft.Storage/storageAccounts ``` **Audit from PowerShell** ``` Get-AzResourceLock -ResourceGroupName ` -ResourceName ` -ResourceType Microsoft.Storage/storageAccounts ``` **Audit from Azure Policy** There is currently no built-in Microsoft policy to audit resource locks on storage accounts. Custom and community policy definitions can check for the existence of a “Microsoft.Authorization/locks” resource with an AuditIfNotExists effect.", + "AdditionalInformation": "", + "References": "https://learn.microsoft.com/en-us/azure/storage/common/lock-account-resource:https://learn.microsoft.com/en-us/azure/azure-resource-manager/management/lock-resources:https://github.com/Azure/azure-rest-api-specs/tree/main/specification/storage", + "DefaultValue": "By default, no locks are applied to Azure resources, including storage accounts. Locks must be manually configured after resource creation." + } + ] + }, + { + "Id": "9.3.11", + "Description": "Ensure Redundancy is set to 'geo-redundant storage (GRS)' on critical Azure Storage Accounts", + "Checks": [ + "storage_geo_redundant_enabled" + ], + "Attributes": [ + { + "Section": "9 Storage Services", + "SubSection": "9.3 Storage Accounts", + "Profile": "Level 2", + "AssessmentStatus": "Automated", + "Description": "Geo-redundant storage (GRS) in Azure replicates data three times within the primary region using locally redundant storage (LRS) and asynchronously copies it to a secondary region hundreds of miles away. This setup ensures high availability and resilience by providing 16 nines (99.99999999999999%) durability over a year, safeguarding data against regional outages.", + "RationaleStatement": "Enabling GRS protects critical data from regional failures by maintaining a copy in a geographically separate location. This significantly reduces the risk of data loss, supports business continuity, and meets high availability requirements for disaster recovery.", + "ImpactStatement": "Enabling geo-redundant storage on Azure storage accounts increases costs due to cross-region data replication.", + "RemediationProcedure": "**Remediate from Azure Portal** 1. Go to `Storage accounts`. 1. Click on a storage account. 1. Under `Data management`, click `Redundancy`. 1. From the `Redundancy` drop-down menu, select `Geo-redundant storage (GRS)`. 1. Click `Save`. 1. Repeat steps 1-5 for each storage account requiring remediation. **Remediate from Azure CLI** For each storage account requiring remediation, run the following command to enable geo-redundant storage: ``` az storage account update --resource-group --name --sku Standard_GRS ``` **Remediate from PowerShell** For each storage account requiring remediation, run the following command to enable geo-redundant storage: ``` Set-AzStorageAccount -ResourceGroupName -Name -SkuName Standard_GRS ```", + "AuditProcedure": "**Audit from Azure Portal** 1. Go to `Storage accounts`. 1. Click on a storage account. 1. Under `Data management`, click `Redundancy`. 1. Ensure that `Redundancy` is set to `Geo-redundant storage (GRS)`. 1. Repeat steps 1-4 for each storage account. **Audit from Azure CLI** Run the following command to list storage accounts: ``` az storage account list ``` For each storage account, run the following command: ``` az storage account show --resource-group --name ``` Under `sku`, ensure that `name` is set to `Standard_GRS`. **Audit from PowerShell** Run the following command to list storage accounts: ``` Get-AzStorageAccount ``` Run the following command to get the storage account in a resource group with a given name: ``` $storageAccount = Get-AzStorageAccount -ResourceGroupName -Name ``` Run the following command to get the redundancy setting for the storage account: ``` $storageAccount.SKU.Name ``` Ensure that the command returns `Standard_GRS`. Repeat for each storage account. **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:** [bf045164-79ba-4215-8f95-f8048dc1780b](https://portal.azure.com/#view/Microsoft_Azure_Policy/PolicyDetailBlade/definitionId/%2Fproviders%2FMicrosoft.Authorization%2FpolicyDefinitions%2Fbf045164-79ba-4215-8f95-f8048dc1780b) **- Name:** 'Geo-redundant storage should be enabled for Storage Accounts'", + "AdditionalInformation": "When choosing the best redundancy option, weigh the trade-offs between lower costs and higher availability. Key factors to consider include: - The method of data replication within the primary region. - The replication of data from a primary to a geographically distant secondary region for protection against regional disasters (geo-replication). - The necessity for read access to replicated data in the secondary region during an outage in the primary region (geo-replication with read access).", + "References": "https://learn.microsoft.com/en-us/azure/storage/common/storage-redundancy:https://learn.microsoft.com/en-us/azure/storage/common/redundancy-migration:https://learn.microsoft.com/en-us/cli/azure/storage/account?view=azure-cli-latest#az-storage-account-update:https://learn.microsoft.com/en-us/powershell/module/az.storage/set-azstorageaccount?view=azps-12.4.0:https://learn.microsoft.com/en-us/azure/storage/common/storage-disaster-recovery-guidance", + "DefaultValue": "When creating a storage account in the Azure Portal, the default redundancy setting is geo-redundant storage (GRS). Using the Azure CLI, the default is read-access geo-redundant storage (RA-GRS). In PowerShell, a redundancy level must be explicitly specified during account creation." + } + ] + } + ] +} diff --git a/prowler/compliance/kubernetes/cis_1.12_kubernetes.json b/prowler/compliance/kubernetes/cis_1.12_kubernetes.json new file mode 100644 index 0000000000..1ba1e55a88 --- /dev/null +++ b/prowler/compliance/kubernetes/cis_1.12_kubernetes.json @@ -0,0 +1,2915 @@ +{ + "Framework": "CIS", + "Name": "CIS Kubernetes Benchmark v1.12.0", + "Version": "1.12.0", + "Provider": "Kubernetes", + "Description": "This CIS Kubernetes Benchmark provides prescriptive guidance for establishing a secure configuration posture for Kubernetes v1.32 - v1.34", + "Requirements": [ + { + "Id": "1.1.1", + "Description": "Ensure that the API server pod specification file permissions are set to 600 or more restrictive", + "Checks": [], + "Attributes": [ + { + "Section": "1 Control Plane Components", + "SubSection": "1.1 Control Plane Node Configuration Files", + "Profile": "Level 1", + "AssessmentStatus": "Automated", + "Description": "Ensure that the API server pod specification file has permissions of `600` or more restrictive.", + "RationaleStatement": "The API server pod specification file controls various parameters that set the behavior of the API server. You should restrict its file permissions to maintain the integrity of the file. The file should be writable by only the administrators on the system.", + "ImpactStatement": "None", + "RemediationProcedure": "Run the below command (based on the file location on your system) on the Control Plane node. For example, ``` chmod 600 /etc/kubernetes/manifests/kube-apiserver.yaml ```", + "AuditProcedure": "Run the below command (based on the file location on your system) on the Control Plane node. For example, ``` stat -c %a /etc/kubernetes/manifests/kube-apiserver.yaml ``` Verify that the permissions are `600` or more restrictive.", + "AdditionalInformation": "", + "References": "https://kubernetes.io/docs/admin/kube-apiserver/", + "DefaultValue": "By default, the `kube-apiserver.yaml` file has permissions of `640`." + } + ] + }, + { + "Id": "1.1.2", + "Description": "Ensure that the API server pod specification file ownership is set to root:root", + "Checks": [], + "Attributes": [ + { + "Section": "1 Control Plane Components", + "SubSection": "1.1 Control Plane Node Configuration Files", + "Profile": "Level 1", + "AssessmentStatus": "Automated", + "Description": "Ensure that the API server pod specification file ownership is set to `root:root`.", + "RationaleStatement": "The API server pod specification file controls various parameters that set the behavior of the API server. You should set its file ownership to maintain the integrity of the file. The file should be owned by `root:root`.", + "ImpactStatement": "None", + "RemediationProcedure": "Run the below command (based on the file location on your system) on the Control Plane node. For example, ``` chown root:root /etc/kubernetes/manifests/kube-apiserver.yaml ```", + "AuditProcedure": "Run the below command (based on the file location on your system) on the Control Plane node. For example, ``` stat -c %U:%G /etc/kubernetes/manifests/kube-apiserver.yaml ``` Verify that the ownership is set to `root:root`.", + "AdditionalInformation": "", + "References": "https://kubernetes.io/docs/admin/kube-apiserver/", + "DefaultValue": "By default, the `kube-apiserver.yaml` file ownership is set to `root:root`." + } + ] + }, + { + "Id": "1.1.3", + "Description": "Ensure that the controller manager pod specification file permissions are set to 600 or more restrictive", + "Checks": [], + "Attributes": [ + { + "Section": "1 Control Plane Components", + "SubSection": "1.1 Control Plane Node Configuration Files", + "Profile": "Level 1", + "AssessmentStatus": "Automated", + "Description": "Ensure that the controller manager pod specification file has permissions of `600` or more restrictive.", + "RationaleStatement": "The controller manager pod specification file controls various parameters that set the behavior of the Controller Manager on the master node. You should restrict its file permissions to maintain the integrity of the file. The file should be writable by only the administrators on the system.", + "ImpactStatement": "None", + "RemediationProcedure": "Run the below command (based on the file location on your system) on the Control Plane node. For example, ``` chmod 600 /etc/kubernetes/manifests/kube-controller-manager.yaml ```", + "AuditProcedure": "Run the below command (based on the file location on your system) on the Control Plane node. For example, ``` stat -c %a /etc/kubernetes/manifests/kube-controller-manager.yaml ``` Verify that the permissions are `600` or more restrictive.", + "AdditionalInformation": "", + "References": "https://kubernetes.io/docs/admin/kube-apiserver/", + "DefaultValue": "By default, the `kube-controller-manager.yaml` file has permissions of `640`." + } + ] + }, + { + "Id": "1.1.4", + "Description": "Ensure that the controller manager pod specification file ownership is set to root:root", + "Checks": [], + "Attributes": [ + { + "Section": "1 Control Plane Components", + "SubSection": "1.1 Control Plane Node Configuration Files", + "Profile": "Level 1", + "AssessmentStatus": "Automated", + "Description": "Ensure that the controller manager pod specification file ownership is set to `root:root`.", + "RationaleStatement": "The controller manager pod specification file controls various parameters that set the behavior of various components of the master node. You should set its file ownership to maintain the integrity of the file. The file should be owned by `root:root`.", + "ImpactStatement": "None", + "RemediationProcedure": "Run the below command (based on the file location on your system) on the Control Plane node. For example, ``` chown root:root /etc/kubernetes/manifests/kube-controller-manager.yaml ```", + "AuditProcedure": "Run the below command (based on the file location on your system) on the Control Plane node. For example, ``` stat -c %U:%G /etc/kubernetes/manifests/kube-controller-manager.yaml ``` Verify that the ownership is set to `root:root`.", + "AdditionalInformation": "", + "References": "https://kubernetes.io/docs/admin/kube-controller-manager", + "DefaultValue": "By default, `kube-controller-manager.yaml` file ownership is set to `root:root`." + } + ] + }, + { + "Id": "1.1.5", + "Description": "Ensure that the scheduler pod specification file permissions are set to 600 or more restrictive", + "Checks": [], + "Attributes": [ + { + "Section": "1 Control Plane Components", + "SubSection": "1.1 Control Plane Node Configuration Files", + "Profile": "Level 1", + "AssessmentStatus": "Automated", + "Description": "Ensure that the scheduler pod specification file has permissions of `600` or more restrictive.", + "RationaleStatement": "The scheduler pod specification file controls various parameters that set the behavior of the Scheduler service in the master node. You should restrict its file permissions to maintain the integrity of the file. The file should be writable by only the administrators on the system.", + "ImpactStatement": "None", + "RemediationProcedure": "Run the below command (based on the file location on your system) on the Control Plane node. For example, ``` chmod 600 /etc/kubernetes/manifests/kube-scheduler.yaml ```", + "AuditProcedure": "Run the below command (based on the file location on your system) on the Control Plane node. For example, ``` stat -c %a /etc/kubernetes/manifests/kube-scheduler.yaml ``` Verify that the permissions are `600` or more restrictive.", + "AdditionalInformation": "", + "References": "https://kubernetes.io/docs/admin/kube-scheduler/", + "DefaultValue": "By default, `kube-scheduler.yaml` file has permissions of `640`." + } + ] + }, + { + "Id": "1.1.6", + "Description": "Ensure that the scheduler pod specification file ownership is set to root:root", + "Checks": [], + "Attributes": [ + { + "Section": "1 Control Plane Components", + "SubSection": "1.1 Control Plane Node Configuration Files", + "Profile": "Level 1", + "AssessmentStatus": "Automated", + "Description": "Ensure that the scheduler pod specification file ownership is set to `root:root`.", + "RationaleStatement": "The scheduler pod specification file controls various parameters that set the behavior of the `kube-scheduler` service in the master node. You should set its file ownership to maintain the integrity of the file. The file should be owned by `root:root`.", + "ImpactStatement": "None", + "RemediationProcedure": "Run the below command (based on the file location on your system) on the Control Plane node. For example, ``` chown root:root /etc/kubernetes/manifests/kube-scheduler.yaml ```", + "AuditProcedure": "Run the below command (based on the file location on your system) on the Control Plane node. For example, ``` stat -c %U:%G /etc/kubernetes/manifests/kube-scheduler.yaml ``` Verify that the ownership is set to `root:root`.", + "AdditionalInformation": "", + "References": "https://kubernetes.io/docs/admin/kube-scheduler/", + "DefaultValue": "By default, `kube-scheduler.yaml` file ownership is set to `root:root`." + } + ] + }, + { + "Id": "1.1.7", + "Description": "Ensure that the etcd pod specification file permissions are set to 600 or more restrictive", + "Checks": [], + "Attributes": [ + { + "Section": "1 Control Plane Components", + "SubSection": "1.1 Control Plane Node Configuration Files", + "Profile": "Level 1", + "AssessmentStatus": "Automated", + "Description": "Ensure that the `/etc/kubernetes/manifests/etcd.yaml` file has permissions of `600` or more restrictive.", + "RationaleStatement": "The etcd pod specification file `/etc/kubernetes/manifests/etcd.yaml` controls various parameters that set the behavior of the `etcd` service in the master node. etcd is a highly-available key-value store which Kubernetes uses for persistent storage of all of its REST API object. You should restrict its file permissions to maintain the integrity of the file. The file should be writable by only the administrators on the system.", + "ImpactStatement": "None", + "RemediationProcedure": "Run the below command (based on the file location on your system) on the Control Plane node. For example, ``` chmod 600 /etc/kubernetes/manifests/etcd.yaml ```", + "AuditProcedure": "Run the below command (based on the file location on your system) on the Control Plane node. For example, ``` stat -c %a /etc/kubernetes/manifests/etcd.yaml ``` Verify that the permissions are `600` or more restrictive.", + "AdditionalInformation": "", + "References": "https://coreos.com/etcd:https://kubernetes.io/docs/admin/etcd/", + "DefaultValue": "By default, `/etc/kubernetes/manifests/etcd.yaml` file has permissions of `640`." + } + ] + }, + { + "Id": "1.1.8", + "Description": "Ensure that the etcd pod specification file ownership is set to root:root", + "Checks": [], + "Attributes": [ + { + "Section": "1 Control Plane Components", + "SubSection": "1.1 Control Plane Node Configuration Files", + "Profile": "Level 1", + "AssessmentStatus": "Automated", + "Description": "Ensure that the `/etc/kubernetes/manifests/etcd.yaml` file ownership is set to `root:root`.", + "RationaleStatement": "The etcd pod specification file `/etc/kubernetes/manifests/etcd.yaml` controls various parameters that set the behavior of the `etcd` service in the master node. etcd is a highly-available key-value store which Kubernetes uses for persistent storage of all of its REST API object. You should set its file ownership to maintain the integrity of the file. The file should be owned by `root:root`.", + "ImpactStatement": "None", + "RemediationProcedure": "Run the below command (based on the file location on your system) on the Control Plane node. For example, ``` chown root:root /etc/kubernetes/manifests/etcd.yaml ```", + "AuditProcedure": "Run the below command (based on the file location on your system) on the Control Plane node. For example, ``` stat -c %U:%G /etc/kubernetes/manifests/etcd.yaml ``` Verify that the ownership is set to `root:root`.", + "AdditionalInformation": "", + "References": "https://coreos.com/etcd:https://kubernetes.io/docs/admin/etcd/", + "DefaultValue": "By default, `/etc/kubernetes/manifests/etcd.yaml` file ownership is set to `root:root`." + } + ] + }, + { + "Id": "1.1.9", + "Description": "Ensure that the Container Network Interface file permissions are set to 600 or more restrictive", + "Checks": [], + "Attributes": [ + { + "Section": "1 Control Plane Components", + "SubSection": "1.1 Control Plane Node Configuration Files", + "Profile": "Level 1", + "AssessmentStatus": "Manual", + "Description": "Ensure that the Container Network Interface files have permissions of `600` or more restrictive.", + "RationaleStatement": "Container Network Interface provides various networking options for overlay networking. You should consult their documentation and restrict their respective file permissions to maintain the integrity of those files. Those files should be writable by only the administrators on the system.", + "ImpactStatement": "None", + "RemediationProcedure": "Run the below command (based on the file location on your system) on the Control Plane node. For example, ``` chmod 600 ```", + "AuditProcedure": "Run the below command (based on the file location on your system) on the Control Plane node. For example, ``` stat -c %a ``` Verify that the permissions are `600` or more restrictive.", + "AdditionalInformation": "", + "References": "https://kubernetes.io/docs/concepts/cluster-administration/networking/", + "DefaultValue": "NA" + } + ] + }, + { + "Id": "1.1.10", + "Description": "Ensure that the Container Network Interface file ownership is set to root:root", + "Checks": [], + "Attributes": [ + { + "Section": "1 Control Plane Components", + "SubSection": "1.1 Control Plane Node Configuration Files", + "Profile": "Level 1", + "AssessmentStatus": "Manual", + "Description": "Ensure that the Container Network Interface files have ownership set to `root:root`.", + "RationaleStatement": "Container Network Interface provides various networking options for overlay networking. You should consult their documentation and restrict their respective file permissions to maintain the integrity of those files. Those files should be owned by `root:root`.", + "ImpactStatement": "None", + "RemediationProcedure": "Run the below command (based on the file location on your system) on the Control Plane node. For example, ``` chown root:root ```", + "AuditProcedure": "Run the below command (based on the file location on your system) on the Control Plane node. For example, ``` stat -c %U:%G ``` Verify that the ownership is set to `root:root`.", + "AdditionalInformation": "", + "References": "https://kubernetes.io/docs/concepts/cluster-administration/networking/", + "DefaultValue": "NA" + } + ] + }, + { + "Id": "1.1.11", + "Description": "Ensure that the etcd data directory permissions are set to 700 or more restrictive", + "Checks": [], + "Attributes": [ + { + "Section": "1 Control Plane Components", + "SubSection": "1.1 Control Plane Node Configuration Files", + "Profile": "Level 1", + "AssessmentStatus": "Automated", + "Description": "Ensure that the etcd data directory has permissions of `700` or more restrictive.", + "RationaleStatement": "etcd is a highly-available key-value store used by Kubernetes deployments for persistent storage of all of its REST API objects. This data directory should be protected from any unauthorized reads or writes. It should not be readable or writable by any group members or the world.", + "ImpactStatement": "None", + "RemediationProcedure": "On the etcd server node, get the etcd data directory, passed as an argument `--data-dir`, from the below command: ``` ps -ef | grep etcd ``` Run the below command (based on the etcd data directory found above). For example, ``` chmod 700 /var/lib/etcd ```", + "AuditProcedure": "On the etcd server node, get the etcd data directory, passed as an argument `--data-dir`, from the below command: ``` ps -ef | grep etcd ``` Run the below command (based on the etcd data directory found above). For example, ``` stat -c %a /var/lib/etcd ``` Verify that the permissions are `700` or more restrictive.", + "AdditionalInformation": "", + "References": "https://coreos.com/etcd/docs/latest/op-guide/configuration.html#data-dir:https://kubernetes.io/docs/admin/etcd/", + "DefaultValue": "By default, etcd data directory has permissions of `755`." + } + ] + }, + { + "Id": "1.1.12", + "Description": "Ensure that the etcd data directory ownership is set to etcd:etcd", + "Checks": [], + "Attributes": [ + { + "Section": "1 Control Plane Components", + "SubSection": "1.1 Control Plane Node Configuration Files", + "Profile": "Level 1", + "AssessmentStatus": "Automated", + "Description": "Ensure that the etcd data directory ownership is set to `etcd:etcd`.", + "RationaleStatement": "etcd is a highly-available key-value store used by Kubernetes deployments for persistent storage of all of its REST API objects. This data directory should be protected from any unauthorized reads or writes. It should be owned by `etcd:etcd`.", + "ImpactStatement": "None", + "RemediationProcedure": "On the etcd server node, get the etcd data directory, passed as an argument `--data-dir`, from the below command: ``` ps -ef | grep etcd ``` Run the below command (based on the etcd data directory found above). For example, ``` chown etcd:etcd /var/lib/etcd ```", + "AuditProcedure": "On the etcd server node, get the etcd data directory, passed as an argument `--data-dir`, from the below command: ``` ps -ef | grep etcd ``` Run the below command (based on the etcd data directory found above). For example, ``` stat -c %U:%G /var/lib/etcd ``` Verify that the ownership is set to `etcd:etcd`.", + "AdditionalInformation": "", + "References": "https://coreos.com/etcd/docs/latest/op-guide/configuration.html#data-dir:https://kubernetes.io/docs/admin/etcd/", + "DefaultValue": "By default, etcd data directory ownership is set to `etcd:etcd`." + } + ] + }, + { + "Id": "1.1.13", + "Description": "Ensure that the default administrative credential file permissions are set to 600", + "Checks": [], + "Attributes": [ + { + "Section": "1 Control Plane Components", + "SubSection": "1.1 Control Plane Node Configuration Files", + "Profile": "Level 1", + "AssessmentStatus": "Automated", + "Description": "Ensure that the `admin.conf` file (and `super-admin.conf` file, where it exists) have permissions of `600`.", + "RationaleStatement": "As part of initial cluster setup, default kubeconfig files are created to be used by the administrator of the cluster. These files contain private keys and certificates which allow for privileged access to the cluster. You should restrict their file permissions to maintain the integrity and confidentiality of the file(s). The file(s) should be readable and writable by only the administrators on the system.", + "ImpactStatement": "None.", + "RemediationProcedure": "Run the below command (based on the file location on your system) on the Control Plane node. For example, ``` chmod 600 /etc/kubernetes/admin.conf ``` On Kubernetes 1.29+ the `super-admin.conf` file should also be modified, if present. For example, ``` chmod 600 /etc/kubernetes/super-admin.conf ```", + "AuditProcedure": "Run the following command (based on the file location on your system) on the Control Plane node. For example, ``` stat -c %a /etc/kubernetes/admin.conf ``` On Kubernetes version 1.29 and higher run the following command as well :- ``` stat -c %a /etc/kubernetes/super-admin.conf ``` Verify that the permissions are `600` or more restrictive.", + "AdditionalInformation": "", + "References": "https://kubernetes.io/docs/setup/independent/create-cluster-kubeadm/:https://raesene.github.io/blog/2024/01/06/when-is-admin-not-admin/", + "DefaultValue": "By default, admin.conf and super-admin.conf have permissions of `600`." + } + ] + }, + { + "Id": "1.1.14", + "Description": "Ensure that the default administrative credential file ownership is set to root:root", + "Checks": [], + "Attributes": [ + { + "Section": "1 Control Plane Components", + "SubSection": "1.1 Control Plane Node Configuration Files", + "Profile": "Level 1", + "AssessmentStatus": "Automated", + "Description": "Ensure that the `admin.conf` (and `super-admin.conf` file, where it exists) file ownership is set to `root:root`.", + "RationaleStatement": "As part of initial cluster setup, default kubeconfig files are created to be used by the administrator of the cluster. These files contain private keys and certificates which allow for privileged access to the cluster. You should set their file ownership to maintain the integrity and confidentiality of the file. The file(s) should be owned by `root:root`.", + "ImpactStatement": "None.", + "RemediationProcedure": "Run the below command (based on the file location on your system) on the Control Plane node. For example, ``` chown root:root /etc/kubernetes/admin.conf ``` On Kubernetes 1.29+ the super-admin.conf file should also be modified, if present. For example, ``` chown root:root /etc/kubernetes/super-admin.conf ```", + "AuditProcedure": "Run the below command (based on the file location on your system) on the Control Plane node. For example, ``` stat -c %U:%G /etc/kubernetes/admin.conf ``` On Kubernetes version 1.29 and higher run the following command as well :- ``` stat -c %U:%G /etc/kubernetes/super-admin.conf ``` Verify that the ownership is set to `root:root`.", + "AdditionalInformation": "", + "References": "https://kubernetes.io/docs/admin/kubeadm/:https://raesene.github.io/blog/2024/01/06/when-is-admin-not-admin/", + "DefaultValue": "By default, `admin.conf` and `super-admin.conf` file ownership is set to `root:root`." + } + ] + }, + { + "Id": "1.1.15", + "Description": "Ensure that the scheduler.conf file permissions are set to 600 or more restrictive", + "Checks": [], + "Attributes": [ + { + "Section": "1 Control Plane Components", + "SubSection": "1.1 Control Plane Node Configuration Files", + "Profile": "Level 1", + "AssessmentStatus": "Automated", + "Description": "Ensure that the `scheduler.conf` file has permissions of `600` or more restrictive.", + "RationaleStatement": "The `scheduler.conf` file is the kubeconfig file for the Scheduler. You should restrict its file permissions to maintain the integrity of the file. The file should be writable by only the administrators on the system.", + "ImpactStatement": "None", + "RemediationProcedure": "Run the below command (based on the file location on your system) on the Control Plane node. For example, ``` chmod 600 /etc/kubernetes/scheduler.conf ```", + "AuditProcedure": "Run the following command (based on the file location on your system) on the Control Plane node. For example, ``` stat -c %a /etc/kubernetes/scheduler.conf ``` Verify that the permissions are `600` or more restrictive.", + "AdditionalInformation": "", + "References": "https://kubernetes.io/docs/setup/independent/create-cluster-kubeadm/", + "DefaultValue": "By default, `scheduler.conf` has permissions of `640`." + } + ] + }, + { + "Id": "1.1.16", + "Description": "Ensure that the scheduler.conf file ownership is set to root:root", + "Checks": [], + "Attributes": [ + { + "Section": "1 Control Plane Components", + "SubSection": "1.1 Control Plane Node Configuration Files", + "Profile": "Level 1", + "AssessmentStatus": "Automated", + "Description": "Ensure that the `scheduler.conf` file ownership is set to `root:root`.", + "RationaleStatement": "The `scheduler.conf` file is the kubeconfig file for the Scheduler. You should set its file ownership to maintain the integrity of the file. The file should be owned by `root:root`.", + "ImpactStatement": "None", + "RemediationProcedure": "Run the below command (based on the file location on your system) on the Control Plane node. For example, ``` chown root:root /etc/kubernetes/scheduler.conf ```", + "AuditProcedure": "Run the below command (based on the file location on your system) on the Control Plane node. For example, ``` stat -c %U:%G /etc/kubernetes/scheduler.conf ``` Verify that the ownership is set to `root:root`.", + "AdditionalInformation": "", + "References": "https://kubernetes.io/docs/admin/kubeadm/", + "DefaultValue": "By default, `scheduler.conf` file ownership is set to `root:root`." + } + ] + }, + { + "Id": "1.1.17", + "Description": "Ensure that the controller-manager.conf file permissions are set to 600 or more restrictive", + "Checks": [], + "Attributes": [ + { + "Section": "1 Control Plane Components", + "SubSection": "1.1 Control Plane Node Configuration Files", + "Profile": "Level 1", + "AssessmentStatus": "Automated", + "Description": "Ensure that the `controller-manager.conf` file has permissions of 600 or more restrictive.", + "RationaleStatement": "The `controller-manager.conf` file is the kubeconfig file for the Controller Manager. You should restrict its file permissions to maintain the integrity of the file. The file should be writable by only the administrators on the system.", + "ImpactStatement": "None", + "RemediationProcedure": "Run the below command (based on the file location on your system) on the Control Plane node. For example, ``` chmod 600 /etc/kubernetes/controller-manager.conf ```", + "AuditProcedure": "Run the following command (based on the file location on your system) on the Control Plane node. For example, ``` stat -c %a /etc/kubernetes/controller-manager.conf ``` Verify that the permissions are `600` or more restrictive.", + "AdditionalInformation": "", + "References": "https://kubernetes.io/docs/admin/kube-controller-manager/", + "DefaultValue": "By default, `controller-manager.conf` has permissions of `640`." + } + ] + }, + { + "Id": "1.1.18", + "Description": "Ensure that the controller-manager.conf file ownership is set to root:root", + "Checks": [], + "Attributes": [ + { + "Section": "1 Control Plane Components", + "SubSection": "1.1 Control Plane Node Configuration Files", + "Profile": "Level 1", + "AssessmentStatus": "Automated", + "Description": "Ensure that the `controller-manager.conf` file ownership is set to `root:root`.", + "RationaleStatement": "The `controller-manager.conf` file is the kubeconfig file for the Controller Manager. You should set its file ownership to maintain the integrity of the file. The file should be owned by `root:root`.", + "ImpactStatement": "None", + "RemediationProcedure": "Run the below command (based on the file location on your system) on the Control Plane node. For example, ``` chown root:root /etc/kubernetes/controller-manager.conf ```", + "AuditProcedure": "Run the below command (based on the file location on your system) on the Control Plane node. For example, ``` stat -c %U:%G /etc/kubernetes/controller-manager.conf ``` Verify that the ownership is set to `root:root`.", + "AdditionalInformation": "", + "References": "https://kubernetes.io/docs/admin/kube-controller-manager/", + "DefaultValue": "By default, `controller-manager.conf` file ownership is set to `root:root`." + } + ] + }, + { + "Id": "1.1.19", + "Description": "Ensure that the Kubernetes PKI directory and file ownership is set to root:root", + "Checks": [], + "Attributes": [ + { + "Section": "1 Control Plane Components", + "SubSection": "1.1 Control Plane Node Configuration Files", + "Profile": "Level 1", + "AssessmentStatus": "Automated", + "Description": "Ensure that the Kubernetes PKI directory and file ownership is set to `root:root`.", + "RationaleStatement": "Kubernetes makes use of a number of certificates as part of its operation. You should set the ownership of the directory containing the PKI information and all files in that directory to maintain their integrity. The directory and files should be owned by `root:root`.", + "ImpactStatement": "None", + "RemediationProcedure": "Run the below command (based on the file location on your system) on the Control Plane node. For example, ``` chown -R root:root /etc/kubernetes/pki/ ```", + "AuditProcedure": "Run the below command (based on the file location on your system) on the Control Plane node. For example, ``` ls -laR /etc/kubernetes/pki/ ``` Verify that the ownership of all files and directories in this hierarchy is set to `root:root`.", + "AdditionalInformation": "", + "References": "https://kubernetes.io/docs/admin/kube-apiserver/", + "DefaultValue": "By default, the /etc/kubernetes/pki/ directory and all of the files and directories contained within it, are set to be owned by the root user." + } + ] + }, + { + "Id": "1.1.20", + "Description": "Ensure that the Kubernetes PKI certificate file permissions are set to 644 or more restrictive", + "Checks": [], + "Attributes": [ + { + "Section": "1 Control Plane Components", + "SubSection": "1.1 Control Plane Node Configuration Files", + "Profile": "Level 1", + "AssessmentStatus": "Manual", + "Description": "Ensure that Kubernetes PKI certificate files have permissions of `644` or more restrictive.", + "RationaleStatement": "Kubernetes makes use of a number of certificate files as part of the operation of its components. The permissions on these files should be set to `644` or more restrictive to protect their integrity and confidentiality.", + "ImpactStatement": "None", + "RemediationProcedure": "Run the below command (based on the file location on your system) on the Control Plane node. For example, ``` chmod -R 644 /etc/kubernetes/pki/*.crt ```", + "AuditProcedure": "Run the below command (based on the file location on your system) on the Control Plane node. For example, ``` stat -c '%a' /etc/kubernetes/pki/*.crt ``` Verify that the permissions are `644` or more restrictive. or ``` ls -l /etc/kubernetes/pki/*.crt ``` Verify -rw------", + "AdditionalInformation": "", + "References": "https://kubernetes.io/docs/admin/kube-apiserver/", + "DefaultValue": "By default, the certificates used by Kubernetes are set to have permissions of `644`" + } + ] + }, + { + "Id": "1.1.21", + "Description": "Ensure that the Kubernetes PKI key file permissions are set to 600", + "Checks": [], + "Attributes": [ + { + "Section": "1 Control Plane Components", + "SubSection": "1.1 Control Plane Node Configuration Files", + "Profile": "Level 1", + "AssessmentStatus": "Manual", + "Description": "Ensure that Kubernetes PKI key files have permissions of `600`.", + "RationaleStatement": "Kubernetes makes use of a number of key files as part of the operation of its components. The permissions on these files should be set to `600` to protect their integrity and confidentiality.", + "ImpactStatement": "None", + "RemediationProcedure": "Run the below command (based on the file location on your system) on the Control Plane node. For example, ``` chmod -R 600 /etc/kubernetes/pki/*.key ```", + "AuditProcedure": "Run the below command (based on the file location on your system) on the Control Plane node. For example, ``` stat -c '%a' /etc/kubernetes/pki/*.key ``` Verify that the permissions are `600` or more restrictive. or ``` ls -l /etc/kubernetes/pki/*.key ``` Verify that the permissions are `-rw------`", + "AdditionalInformation": "", + "References": "https://kubernetes.io/docs/admin/kube-apiserver/", + "DefaultValue": "By default, the keys used by Kubernetes are set to have permissions of `600`" + } + ] + }, + { + "Id": "1.2.1", + "Description": "Ensure that the --anonymous-auth argument is set to false", + "Checks": [ + "apiserver_anonymous_requests" + ], + "Attributes": [ + { + "Section": "1 Control Plane Components", + "SubSection": "1.2 API Server", + "Profile": "Level 1", + "AssessmentStatus": "Manual", + "Description": "Disable anonymous requests to the API server.", + "RationaleStatement": "When enabled, requests that are not rejected by other configured authentication methods are treated as anonymous requests. These requests are then served by the API server. You should rely on authentication to authorize access and disallow anonymous requests. If you are using RBAC authorization, it is generally considered reasonable to allow anonymous access to the API Server for health checks and discovery purposes, and hence this recommendation is not scored. However, you should consider whether anonymous discovery is an acceptable risk for your purposes.", + "ImpactStatement": "Anonymous requests will be rejected.", + "RemediationProcedure": "Edit the API server pod specification file `/etc/kubernetes/manifests/kube-apiserver.yaml` on the Control Plane node and set the below parameter. ``` --anonymous-auth=false ```", + "AuditProcedure": "Run the following command on the Control Plane node: ``` ps -ef | grep kube-apiserver ``` Verify that the `--anonymous-auth` argument is set to `false`. Alternative Audit Method ``` kubectl get pod -nkube-system -lcomponent=kube-apiserver -o=jsonpath='{range .items[*]}{.spec.containers[*].command} {\\}{end}' | grep '--anonymous-auth' | grep -i false ``` If the exit code is '1', then the control isn't present / failed", + "AdditionalInformation": "", + "References": "https://kubernetes.io/docs/admin/kube-apiserver/:https://kubernetes.io/docs/admin/authentication/#anonymous-requests", + "DefaultValue": "By default, anonymous access is enabled." + } + ] + }, + { + "Id": "1.2.2", + "Description": "Ensure that the --token-auth-file parameter is not set", + "Checks": [ + "apiserver_no_token_auth_file" + ], + "Attributes": [ + { + "Section": "1 Control Plane Components", + "SubSection": "1.2 API Server", + "Profile": "Level 1", + "AssessmentStatus": "Automated", + "Description": "Do not use token based authentication.", + "RationaleStatement": "The token-based authentication utilizes static tokens to authenticate requests to the apiserver. The tokens are stored in clear-text in a file on the apiserver, and cannot be revoked or rotated without restarting the apiserver. Hence, do not use static token-based authentication.", + "ImpactStatement": "You will have to configure and use alternate authentication mechanisms such as certificates. Static token based authentication could not be used.", + "RemediationProcedure": "Follow the documentation and configure alternate mechanisms for authentication. Then, edit the API server pod specification file `/etc/kubernetes/manifests/kube-apiserver.yaml` on the master node and remove the `--token-auth-file=` parameter.", + "AuditProcedure": "Run the following command on the Control Plane node: ``` ps -ef | grep kube-apiserver ``` Verify that the `--token-auth-file` argument does not exist. Alternative Audit Method ``` kubectl get pod -nkube-system -lcomponent=kube-apiserver -o=jsonpath='{range .items[*]}{.spec.containers[*].command} {\\}{end}' | grep '--token-auth-file' | grep -i false ``` If the exit code is '1', then the control isn't present / failed", + "AdditionalInformation": "", + "References": "https://kubernetes.io/docs/admin/authentication/#static-token-file:https://kubernetes.io/docs/admin/kube-apiserver/", + "DefaultValue": "By default, `--token-auth-file` argument is not set." + } + ] + }, + { + "Id": "1.2.3", + "Description": "Ensure that the DenyServiceExternalIPs is set", + "Checks": [ + "apiserver_deny_service_external_ips" + ], + "Attributes": [ + { + "Section": "1 Control Plane Components", + "SubSection": "1.2 API Server", + "Profile": "Level 1", + "AssessmentStatus": "Manual", + "Description": "This admission controller rejects all net-new usage of the Service field externalIPs.", + "RationaleStatement": "Most users do not need the ability to set the `externalIPs` field for a `Service` at all, and cluster admins should consider disabling this functionality by enabling the `DenyServiceExternalIPs` admission controller. Clusters that do need to allow this functionality should consider using some custom policy to manage its usage.", + "ImpactStatement": "When enabled, users of the cluster may not create new Services which use externalIPs and may not add new values to externalIPs on existing Service objects.", + "RemediationProcedure": "Edit the API server pod specification file `/etc/kubernetes/manifests/kube-apiserver.yaml` on the master node and append the Kubernetes API server flag --enable-admission-plugins with the DenyServiceExternalIPs plugin. Note, the Kubernetes API server flag --enable-admission-plugins takes a comma-delimited list of admission control plugins to be enabled, even if they are in the list of plugins enabled by default. ``` kube-apiserver --enable-admission-plugins=DenyServiceExternalIPs ```", + "AuditProcedure": "Run the following command on the Control Plane node: ``` ps -ef | grep kube-apiserver ``` Verify that the `DenyServiceExternalIPs' argument exist as a string value in --enable-admission-plugins.", + "AdditionalInformation": "", + "References": "https://kubernetes.io/docs/reference/access-authn-authz/admission-controllers/:https://kubernetes.io/docs/admin/kube-apiserver/", + "DefaultValue": "By default, --enable-admission-plugins=DenyServiceExternalIP argument is not set, and the use of externalIPs is authorized." + } + ] + }, + { + "Id": "1.2.4", + "Description": "Ensure that the --kubelet-client-certificate and --kubelet-client-key arguments are set as appropriate", + "Checks": [ + "apiserver_kubelet_tls_auth" + ], + "Attributes": [ + { + "Section": "1 Control Plane Components", + "SubSection": "1.2 API Server", + "Profile": "Level 1", + "AssessmentStatus": "Automated", + "Description": "Enable certificate based kubelet authentication.", + "RationaleStatement": "The apiserver, by default, does not authenticate itself to the kubelet's HTTPS endpoints. The requests from the apiserver are treated anonymously. You should set up certificate-based kubelet authentication to ensure that the apiserver authenticates itself to kubelets when submitting requests.", + "ImpactStatement": "You require TLS to be configured on apiserver as well as kubelets.", + "RemediationProcedure": "Follow the Kubernetes documentation and set up the TLS connection between the apiserver and kubelets. Then, edit API server pod specification file `/etc/kubernetes/manifests/kube-apiserver.yaml` on the Control Plane node and set the kubelet client certificate and key parameters as below. ``` --kubelet-client-certificate= --kubelet-client-key= ```", + "AuditProcedure": "Run the following command on the Control Plane node: ``` ps -ef | grep kube-apiserver ``` Verify that the `--kubelet-client-certificate` and `--kubelet-client-key` arguments exist and they are set as appropriate. Alternative Audit ``` kubectl get pod -nkube-system -lcomponent=kube-apiserver -o=jsonpath='{range .items[*]}{.spec.containers[*].command} {\\}{end}' | grep '--kubelet-client-certificate' | grep -i false ``` If the exit code is '1', then the control isn't present / failed", + "AdditionalInformation": "", + "References": "https://kubernetes.io/docs/admin/kube-apiserver/:https://kubernetes.io/docs/admin/kubelet-authentication-authorization/:https://kubernetes.io/docs/concepts/cluster-administration/master-node-communication/#apiserver---kubelet", + "DefaultValue": "By default, certificate-based kubelet authentication is not set." + } + ] + }, + { + "Id": "1.2.5", + "Description": "Ensure that the --kubelet-certificate-authority argument is set as appropriate", + "Checks": [ + "apiserver_kubelet_cert_auth" + ], + "Attributes": [ + { + "Section": "1 Control Plane Components", + "SubSection": "1.2 API Server", + "Profile": "Level 1", + "AssessmentStatus": "Automated", + "Description": "Verify kubelet's certificate before establishing connection.", + "RationaleStatement": "The connections from the apiserver to the kubelet are used for fetching logs for pods, attaching (through kubectl) to running pods, and using the kubelet’s port-forwarding functionality. These connections terminate at the kubelet’s HTTPS endpoint. By default, the apiserver does not verify the kubelet’s serving certificate, which makes the connection subject to man-in-the-middle attacks, and unsafe to run over untrusted and/or public networks.", + "ImpactStatement": "You require TLS to be configured on apiserver as well as kubelets.", + "RemediationProcedure": "Follow the Kubernetes documentation and setup the TLS connection between the apiserver and kubelets. Then, edit the API server pod specification file `/etc/kubernetes/manifests/kube-apiserver.yaml` on the Control Plane node and set the `--kubelet-certificate-authority` parameter to the path to the cert file for the certificate authority. ``` --kubelet-certificate-authority= ```", + "AuditProcedure": "Run the following command on the Control Plane node: ``` ps -ef | grep kube-apiserver ``` Verify that the `--kubelet-certificate-authority` argument exists and is set as appropriate. Alternative Audit ``` kubectl get pod -nkube-system -lcomponent=kube-apiserver -o=jsonpath='{range .items[]}{.spec.containers[].command} {\\}{end}' | grep '--kubelet-certificate-authority' | grep -i false ``` If the exit code is '1', then the control isn't present / failed", + "AdditionalInformation": "", + "References": "https://kubernetes.io/docs/admin/kube-apiserver/:https://kubernetes.io/docs/admin/kubelet-authentication-authorization/:https://kubernetes.io/docs/concepts/cluster-administration/master-node-communication/#apiserver---kubelet", + "DefaultValue": "By default, `--kubelet-certificate-authority` argument is not set." + } + ] + }, + { + "Id": "1.2.6", + "Description": "Ensure that the --authorization-mode argument is not set to AlwaysAllow", + "Checks": [ + "apiserver_auth_mode_not_always_allow" + ], + "Attributes": [ + { + "Section": "1 Control Plane Components", + "SubSection": "1.2 API Server", + "Profile": "Level 1", + "AssessmentStatus": "Automated", + "Description": "Do not always authorize all requests.", + "RationaleStatement": "The API Server, can be configured to allow all requests. This mode should not be used on any production cluster.", + "ImpactStatement": "Only authorized requests will be served.", + "RemediationProcedure": "Edit the API server pod specification file `/etc/kubernetes/manifests/kube-apiserver.yaml` on the Control Plane node and set the `--authorization-mode` parameter to values other than `AlwaysAllow`. One such example could be as below. ``` --authorization-mode=RBAC ```", + "AuditProcedure": "Run the following command on the Control Plane node: ``` ps -ef | grep kube-apiserver ``` Verify that the `--authorization-mode` argument exists and is not set to `AlwaysAllow`.", + "AdditionalInformation": "", + "References": "https://kubernetes.io/docs/reference/command-line-tools-reference/kube-apiserver/:https://kubernetes.io/docs/admin/authorization/", + "DefaultValue": "By default, `AlwaysAllow` is not enabled." + } + ] + }, + { + "Id": "1.2.7", + "Description": "Ensure that the --authorization-mode argument includes Node", + "Checks": [ + "apiserver_auth_mode_include_node" + ], + "Attributes": [ + { + "Section": "1 Control Plane Components", + "SubSection": "1.2 API Server", + "Profile": "Level 1", + "AssessmentStatus": "Automated", + "Description": "Restrict kubelet nodes to reading only objects associated with them.", + "RationaleStatement": "The `Node` authorization mode only allows kubelets to read `Secret`, `ConfigMap`, `PersistentVolume`, and `PersistentVolumeClaim` objects associated with their nodes.", + "ImpactStatement": "None", + "RemediationProcedure": "Edit the API server pod specification file `/etc/kubernetes/manifests/kube-apiserver.yaml` on the Control Plane node and set the `--authorization-mode` parameter to a value that includes `Node`. ``` --authorization-mode=Node,RBAC ```", + "AuditProcedure": "Run the following command on the Control Plane node: ``` ps -ef | grep kube-apiserver ``` Verify that the `--authorization-mode` argument exists and is set to a value to include `Node`.", + "AdditionalInformation": "", + "References": "https://kubernetes.io/docs/admin/kube-apiserver/:https://kubernetes.io/docs/admin/authorization/node/:https://github.com/kubernetes/kubernetes/pull/46076:https://acotten.com/post/kube17-security", + "DefaultValue": "By default, `Node` authorization is not enabled." + } + ] + }, + { + "Id": "1.2.8", + "Description": "Ensure that the --authorization-mode argument includes RBAC", + "Checks": [ + "apiserver_auth_mode_include_rbac" + ], + "Attributes": [ + { + "Section": "1 Control Plane Components", + "SubSection": "1.2 API Server", + "Profile": "Level 1", + "AssessmentStatus": "Automated", + "Description": "Turn on Role Based Access Control.", + "RationaleStatement": "Role Based Access Control (RBAC) allows fine-grained control over the operations that different entities can perform on different objects in the cluster. It is recommended to use the RBAC authorization mode.", + "ImpactStatement": "When RBAC is enabled you will need to ensure that appropriate RBAC settings (including Roles, RoleBindings and ClusterRoleBindings) are configured to allow appropriate access.", + "RemediationProcedure": "Edit the API server pod specification file `/etc/kubernetes/manifests/kube-apiserver.yaml` on the Control Plane node and set the `--authorization-mode` parameter to a value that includes `RBAC`, for example: ``` --authorization-mode=Node,RBAC ```", + "AuditProcedure": "Run the following command on the Control Plane node: ``` ps -ef | grep kube-apiserver ``` Verify that the `--authorization-mode` argument exists and is set to a value to include `RBAC`.", + "AdditionalInformation": "", + "References": "https://kubernetes.io/docs/reference/access-authn-authz/rbac/", + "DefaultValue": "By default, `RBAC` authorization is not enabled." + } + ] + }, + { + "Id": "1.2.9", + "Description": "Ensure that the admission control plugin EventRateLimit is set", + "Checks": [ + "apiserver_event_rate_limit" + ], + "Attributes": [ + { + "Section": "1 Control Plane Components", + "SubSection": "1.2 API Server", + "Profile": "Level 1", + "AssessmentStatus": "Manual", + "Description": "Limit the rate at which the API server accepts requests.", + "RationaleStatement": "Using `EventRateLimit` admission control enforces a limit on the number of events that the API Server will accept in a given time slice. A misbehaving workload could overwhelm and DoS the API Server, making it unavailable. This particularly applies to a multi-tenant cluster, where there might be a small percentage of misbehaving tenants which could have a significant impact on the performance of the cluster overall. Hence, it is recommended to limit the rate of events that the API server will accept.", + "ImpactStatement": "You need to carefully tune in limits as per your environment.", + "RemediationProcedure": "Follow the Kubernetes documentation and set the desired limits in a configuration file. Then, edit the API server pod specification file `/etc/kubernetes/manifests/kube-apiserver.yaml` and set the below parameters. ``` --enable-admission-plugins=...,EventRateLimit,... --admission-control-config-file= ```", + "AuditProcedure": "Run the following command on the Control Plane node: ``` ps -ef | grep kube-apiserver ``` Verify that the `--enable-admission-plugins` argument is set to a value that includes `EventRateLimit`.", + "AdditionalInformation": "", + "References": "https://kubernetes.io/docs/reference/command-line-tools-reference/kube-apiserver/:https://kubernetes.io/docs/reference/access-authn-authz/admission-controllers/#eventratelimit:https://github.com/staebler/community/blob/9873b632f4d99b5d99c38c9b15fe2f8b93d0a746/contributors/design-proposals/admission_control_event_rate_limit.md", + "DefaultValue": "By default, `EventRateLimit` is not set." + } + ] + }, + { + "Id": "1.2.10", + "Description": "Ensure that the admission control plugin AlwaysAdmit is not set", + "Checks": [ + "apiserver_no_always_admit_plugin" + ], + "Attributes": [ + { + "Section": "1 Control Plane Components", + "SubSection": "1.2 API Server", + "Profile": "Level 1", + "AssessmentStatus": "Automated", + "Description": "Do not allow all requests.", + "RationaleStatement": "Setting admission control plugin `AlwaysAdmit` allows all requests and do not filter any requests. The `AlwaysAdmit` admission controller was deprecated in Kubernetes v1.13. Its behavior was equivalent to turning off all admission controllers.", + "ImpactStatement": "Only requests explicitly allowed by the admissions control plugins would be served.", + "RemediationProcedure": "Edit the API server pod specification file `/etc/kubernetes/manifests/kube-apiserver.yaml` on the Control Plane node and either remove the `--enable-admission-plugins` parameter, or set it to a value that does not include `AlwaysAdmit`.", + "AuditProcedure": "Run the following command on the Control Plane node: ``` ps -ef | grep kube-apiserver ``` Verify that if the `--enable-admission-plugins` argument is set, its value does not include `AlwaysAdmit`.", + "AdditionalInformation": "", + "References": "https://kubernetes.io/docs/reference/command-line-tools-reference/kube-apiserver/:https://kubernetes.io/docs/reference/access-authn-authz/admission-controllers/#alwaysadmit", + "DefaultValue": "`AlwaysAdmit` is not in the list of default admission plugins." + } + ] + }, + { + "Id": "1.2.11", + "Description": "Ensure that the admission control plugin AlwaysPullImages is set", + "Checks": [ + "apiserver_always_pull_images_plugin" + ], + "Attributes": [ + { + "Section": "1 Control Plane Components", + "SubSection": "1.2 API Server", + "Profile": "Level 1", + "AssessmentStatus": "Manual", + "Description": "Always pull images.", + "RationaleStatement": "Setting admission control policy to `AlwaysPullImages` forces every new pod to pull the required images every time. In a multi-tenant cluster users can be assured that their private images can only be used by those who have the credentials to pull them. Without this admission control policy, once an image has been pulled to a node, any pod from any user can use it simply by knowing the image’s name, without any authorization check against the image ownership. When this plug-in is enabled, images are always pulled prior to starting containers, which means valid credentials are required.", + "ImpactStatement": "Credentials would be required to pull the private images every time. Also, in trusted environments, this might increases load on network, registry, and decreases speed. This setting could impact offline or isolated clusters, which have images preloaded and do not have access to a registry to pull in-use images. This setting is not appropriate for clusters which use this configuration.", + "RemediationProcedure": "Edit the API server pod specification file `/etc/kubernetes/manifests/kube-apiserver.yaml` on the Control Plane node and set the `--enable-admission-plugins` parameter to include `AlwaysPullImages`. ``` --enable-admission-plugins=...,AlwaysPullImages,... ```", + "AuditProcedure": "Run the following command on the Control Plane node: ``` ps -ef | grep kube-apiserver ``` Verify that the `--enable-admission-plugins` argument is set to a value that includes `AlwaysPullImages`.", + "AdditionalInformation": "", + "References": "https://kubernetes.io/docs/reference/command-line-tools-reference/kube-apiserver/:https://kubernetes.io/docs/reference/access-authn-authz/admission-controllers/#alwayspullimages", + "DefaultValue": "By default, `AlwaysPullImages` is not set." + } + ] + }, + { + "Id": "1.2.12", + "Description": "Ensure that the admission control plugin ServiceAccount is set", + "Checks": [ + "apiserver_service_account_plugin" + ], + "Attributes": [ + { + "Section": "1 Control Plane Components", + "SubSection": "1.2 API Server", + "Profile": "Level 2", + "AssessmentStatus": "Automated", + "Description": "Automate service accounts management.", + "RationaleStatement": "When you create a pod, if you do not specify a service account, it is automatically assigned the `default` service account in the same namespace. You should create your own service account and let the API server manage its security tokens.", + "ImpactStatement": "None.", + "RemediationProcedure": "Follow the documentation and create `ServiceAccount` objects as per your environment. Then, edit the API server pod specification file `/etc/kubernetes/manifests/kube-apiserver.yaml` on the master node and ensure that the `--disable-admission-plugins` parameter is set to a value that does not include `ServiceAccount`.", + "AuditProcedure": "Run the following command on the Control Plane node: ``` ps -ef | grep kube-apiserver ``` Verify that the `--disable-admission-plugins` argument is set to a value that does not includes `ServiceAccount`.", + "AdditionalInformation": "", + "References": "https://kubernetes.io/docs/reference/command-line-tools-reference/kube-apiserver/:https://kubernetes.io/docs/reference/access-authn-authz/admission-controllers/#serviceaccount:https://kubernetes.io/docs/tasks/configure-pod-container/configure-service-account/", + "DefaultValue": "By default, `ServiceAccount` is set." + } + ] + }, + { + "Id": "1.2.13", + "Description": "Ensure that the admission control plugin NamespaceLifecycle is set", + "Checks": [ + "apiserver_namespace_lifecycle_plugin" + ], + "Attributes": [ + { + "Section": "1 Control Plane Components", + "SubSection": "1.2 API Server", + "Profile": "Level 2", + "AssessmentStatus": "Automated", + "Description": "Reject creating objects in a namespace that is undergoing termination.", + "RationaleStatement": "Setting admission control policy to `NamespaceLifecycle` ensures that objects cannot be created in non-existent namespaces, and that namespaces undergoing termination are not used for creating the new objects. This is recommended to enforce the integrity of the namespace termination process and also for the availability of the newer objects.", + "ImpactStatement": "None", + "RemediationProcedure": "Edit the API server pod specification file `/etc/kubernetes/manifests/kube-apiserver.yaml` on the Control Plane node and set the `--disable-admission-plugins` parameter to ensure it does not include `NamespaceLifecycle`.", + "AuditProcedure": "Run the following command on the Control Plane node: ``` ps -ef | grep kube-apiserver ``` Verify that the `--disable-admission-plugins` argument is set to a value that does not include `NamespaceLifecycle`.", + "AdditionalInformation": "", + "References": "https://kubernetes.io/docs/reference/command-line-tools-reference/kube-apiserver/:https://kubernetes.io/docs/reference/access-authn-authz/admission-controllers/#namespacelifecycle", + "DefaultValue": "By default, `NamespaceLifecycle` is set." + } + ] + }, + { + "Id": "1.2.14", + "Description": "Ensure that the admission control plugin NodeRestriction is set", + "Checks": [ + "apiserver_node_restriction_plugin" + ], + "Attributes": [ + { + "Section": "1 Control Plane Components", + "SubSection": "1.2 API Server", + "Profile": "Level 2", + "AssessmentStatus": "Automated", + "Description": "Limit the `Node` and `Pod` objects that a kubelet could modify.", + "RationaleStatement": "Using the `NodeRestriction` plug-in ensures that the kubelet is restricted to the `Node` and `Pod` objects that it could modify as defined. Such kubelets will only be allowed to modify their own `Node` API object, and only modify `Pod` API objects that are bound to their node.", + "ImpactStatement": "None", + "RemediationProcedure": "Follow the Kubernetes documentation and configure `NodeRestriction` plug-in on kubelets. Then, edit the API server pod specification file `/etc/kubernetes/manifests/kube-apiserver.yaml` on the master node and set the `--enable-admission-plugins` parameter to a value that includes `NodeRestriction`. ``` --enable-admission-plugins=...,NodeRestriction,... ```", + "AuditProcedure": "Run the following command on the Control Plane node: ``` ps -ef | grep kube-apiserver ``` Verify that the `--enable-admission-plugins` argument is set to a value that includes `NodeRestriction`.", + "AdditionalInformation": "", + "References": "https://kubernetes.io/docs/reference/command-line-tools-reference/kube-apiserver/:https://kubernetes.io/docs/reference/access-authn-authz/admission-controllers/#noderestriction:https://kubernetes.io/docs/reference/access-authn-authz/node/", + "DefaultValue": "By default, `NodeRestriction` is not set." + } + ] + }, + { + "Id": "1.2.15", + "Description": "Ensure that the --profiling argument is set to false", + "Checks": [ + "apiserver_disable_profiling" + ], + "Attributes": [ + { + "Section": "1 Control Plane Components", + "SubSection": "1.2 API Server", + "Profile": "Level 1", + "AssessmentStatus": "Automated", + "Description": "Disable profiling, if not needed.", + "RationaleStatement": "Profiling allows for the identification of specific performance bottlenecks. It generates a significant amount of program data that could potentially be exploited to uncover system and program details. If you are not experiencing any bottlenecks and do not need the profiler for troubleshooting purposes, it is recommended to turn it off to reduce the potential attack surface.", + "ImpactStatement": "Profiling information would not be available.", + "RemediationProcedure": "Edit the API server pod specification file `/etc/kubernetes/manifests/kube-apiserver.yaml` on the Control Plane node and set the below parameter. ``` --profiling=false ```", + "AuditProcedure": "Run the following command on the Control Plane node: ``` ps -ef | grep kube-apiserver ``` Verify that the `--profiling` argument is set to `false`.", + "AdditionalInformation": "", + "References": "https://kubernetes.io/docs/reference/command-line-tools-reference/kube-apiserver/", + "DefaultValue": "By default, profiling is enabled." + } + ] + }, + { + "Id": "1.2.16", + "Description": "Ensure that the --audit-log-path argument is set", + "Checks": [ + "apiserver_audit_log_path_set" + ], + "Attributes": [ + { + "Section": "1 Control Plane Components", + "SubSection": "1.2 API Server", + "Profile": "Level 1", + "AssessmentStatus": "Automated", + "Description": "Enable auditing on the Kubernetes API Server and set the desired audit log path.", + "RationaleStatement": "Auditing the Kubernetes API Server provides a security-relevant chronological set of records documenting the sequence of activities that have affected system by individual users, administrators or other components of the system. Even though currently, Kubernetes provides only basic audit capabilities, it should be enabled. You can enable it by setting an appropriate audit log path.", + "ImpactStatement": "None", + "RemediationProcedure": "Edit the API server pod specification file `/etc/kubernetes/manifests/kube-apiserver.yaml` on the Control Plane node and set the `--audit-log-path` parameter to a suitable path and file where you would like audit logs to be written, for example: ``` --audit-log-path=/var/log/apiserver/audit.log ```", + "AuditProcedure": "Run the following command on the Control Plane node: ``` ps -ef | grep kube-apiserver ``` Verify that the `--audit-log-path` argument is set as appropriate.", + "AdditionalInformation": "", + "References": "https://kubernetes.io/docs/reference/command-line-tools-reference/kube-apiserver/:https://kubernetes.io/docs/tasks/debug/debug-cluster/audit/:https://github.com/kubernetes/enhancements/issues/22", + "DefaultValue": "By default, auditing is not enabled." + } + ] + }, + { + "Id": "1.2.17", + "Description": "Ensure that the --audit-log-maxage argument is set to 30 or as appropriate", + "Checks": [ + "apiserver_audit_log_maxage_set" + ], + "Attributes": [ + { + "Section": "1 Control Plane Components", + "SubSection": "1.2 API Server", + "Profile": "Level 1", + "AssessmentStatus": "Automated", + "Description": "Retain the logs for at least 30 days or as appropriate.", + "RationaleStatement": "Retaining logs for at least 30 days ensures that you can go back in time and investigate or correlate any events. Set your audit log retention period to 30 days or as per your business requirements.", + "ImpactStatement": "None", + "RemediationProcedure": "Edit the API server pod specification file `/etc/kubernetes/manifests/kube-apiserver.yaml` on the Control Plane node and set the `--audit-log-maxage` parameter to 30 or as an appropriate number of days: ``` --audit-log-maxage=30 ```", + "AuditProcedure": "Run the following command on the Control Plane node: ``` ps -ef | grep kube-apiserver ``` Verify that the `--audit-log-maxage` argument is set to `30` or as appropriate.", + "AdditionalInformation": "", + "References": "https://kubernetes.io/docs/reference/command-line-tools-reference/kube-apiserver/:https://kubernetes.io/docs/tasks/debug/debug-cluster/audit/:https://github.com/kubernetes/enhancements/issues/22", + "DefaultValue": "By default, auditing is not enabled." + } + ] + }, + { + "Id": "1.2.18", + "Description": "Ensure that the --audit-log-maxbackup argument is set to 10 or as appropriate", + "Checks": [ + "apiserver_audit_log_maxbackup_set" + ], + "Attributes": [ + { + "Section": "1 Control Plane Components", + "SubSection": "1.2 API Server", + "Profile": "Level 1", + "AssessmentStatus": "Automated", + "Description": "Retain 10 or an appropriate number of old log files.", + "RationaleStatement": "Kubernetes automatically rotates the log files. Retaining old log files ensures that you would have sufficient log data available for carrying out any investigation or correlation. For example, if you have set file size of 100 MB and the number of old log files to keep as 10, you would approximate have 1 GB of log data that you could potentially use for your analysis.", + "ImpactStatement": "None", + "RemediationProcedure": "Edit the API server pod specification file `/etc/kubernetes/manifests/kube-apiserver.yaml` on the Control Plane node and set the `--audit-log-maxbackup` parameter to 10 or to an appropriate value. ``` --audit-log-maxbackup=10 ```", + "AuditProcedure": "Run the following command on the Control Plane node: ``` ps -ef | grep kube-apiserver ``` Verify that the `--audit-log-maxbackup` argument is set to `10` or as appropriate.", + "AdditionalInformation": "", + "References": "https://kubernetes.io/docs/reference/command-line-tools-reference/kube-apiserver/:https://kubernetes.io/docs/tasks/debug/debug-cluster/audit/:https://github.com/kubernetes/enhancements/issues/22", + "DefaultValue": "By default, auditing is not enabled." + } + ] + }, + { + "Id": "1.2.19", + "Description": "Ensure that the --audit-log-maxsize argument is set to 100 or as appropriate", + "Checks": [ + "apiserver_audit_log_maxsize_set" + ], + "Attributes": [ + { + "Section": "1 Control Plane Components", + "SubSection": "1.2 API Server", + "Profile": "Level 1", + "AssessmentStatus": "Automated", + "Description": "Rotate log files on reaching 100 MB or as appropriate.", + "RationaleStatement": "Kubernetes automatically rotates the log files. Retaining old log files ensures that you would have sufficient log data available for carrying out any investigation or correlation. If you have set file size of 100 MB and the number of old log files to keep as 10, you would approximate have 1 GB of log data that you could potentially use for your analysis.", + "ImpactStatement": "None", + "RemediationProcedure": "Edit the API server pod specification file `/etc/kubernetes/manifests/kube-apiserver.yaml` on the Control Plane node and set the `--audit-log-maxsize` parameter to an appropriate size in MB. For example, to set it as 100 MB: ``` --audit-log-maxsize=100 ```", + "AuditProcedure": "Run the following command on the Control Plane node: ``` ps -ef | grep kube-apiserver ``` Verify that the `--audit-log-maxsize` argument is set to `100` or as appropriate.", + "AdditionalInformation": "", + "References": "https://kubernetes.io/docs/reference/command-line-tools-reference/kube-apiserver/:https://kubernetes.io/docs/tasks/debug/debug-cluster/audit/:https://github.com/kubernetes/enhancements/issues/22", + "DefaultValue": "By default, auditing is not enabled." + } + ] + }, + { + "Id": "1.2.20", + "Description": "Ensure that the --request-timeout argument is set as appropriate", + "Checks": [ + "apiserver_request_timeout_set" + ], + "Attributes": [ + { + "Section": "1 Control Plane Components", + "SubSection": "1.2 API Server", + "Profile": "Level 1", + "AssessmentStatus": "Manual", + "Description": "Set global request timeout for API server requests as appropriate.", + "RationaleStatement": "Setting global request timeout allows extending the API server request timeout limit to a duration appropriate to the user's connection speed. By default, it is set to 60 seconds which might be problematic on slower connections making cluster resources inaccessible once the data volume for requests exceeds what can be transmitted in 60 seconds. But, setting this timeout limit to be too large can exhaust the API server resources making it prone to Denial-of-Service attack. Hence, it is recommended to set this limit as appropriate and change the default limit of 60 seconds only if needed.", + "ImpactStatement": "None", + "RemediationProcedure": "Edit the API server pod specification file `/etc/kubernetes/manifests/kube-apiserver.yaml` and set the below parameter as appropriate and if needed. For example, ``` --request-timeout=300s ```", + "AuditProcedure": "Run the following command on the Control Plane node: ``` ps -ef | grep kube-apiserver ``` Verify that the `--request-timeout` argument is either not set or set to an appropriate value.", + "AdditionalInformation": "", + "References": "https://kubernetes.io/docs/reference/command-line-tools-reference/kube-apiserver/:https://github.com/kubernetes/kubernetes/pull/51415", + "DefaultValue": "By default, `--request-timeout` is set to 60 seconds." + } + ] + }, + { + "Id": "1.2.21", + "Description": "Ensure that the --service-account-lookup argument is set to true", + "Checks": [ + "apiserver_service_account_lookup_true" + ], + "Attributes": [ + { + "Section": "1 Control Plane Components", + "SubSection": "1.2 API Server", + "Profile": "Level 1", + "AssessmentStatus": "Automated", + "Description": "Validate service account before validating token.", + "RationaleStatement": "If `--service-account-lookup` is not enabled, the apiserver only verifies that the authentication token is valid, and does not validate that the service account token mentioned in the request is actually present in etcd. This allows using a service account token even after the corresponding service account is deleted. This is an example of time of check to time of use security issue.", + "ImpactStatement": "None", + "RemediationProcedure": "Edit the API server pod specification file `/etc/kubernetes/manifests/kube-apiserver.yaml` on the Control Plane node and set the below parameter. ``` --service-account-lookup=true ``` Alternatively, you can delete the `--service-account-lookup` parameter from this file so that the default takes effect.", + "AuditProcedure": "Run the following command on the Control Plane node: ``` ps -ef | grep kube-apiserver ``` Verify that if the `--service-account-lookup` argument exists it is set to `true`.", + "AdditionalInformation": "", + "References": "https://kubernetes.io/docs/reference/command-line-tools-reference/kube-apiserver/:https://github.com/kubernetes/kubernetes/issues/24167:https://en.wikipedia.org/wiki/Time-of-check_to_time-of-use", + "DefaultValue": "By default, `--service-account-lookup` argument is set to `true`." + } + ] + }, + { + "Id": "1.2.22", + "Description": "Ensure that the --service-account-key-file argument is set as appropriate", + "Checks": [ + "apiserver_service_account_key_file_set" + ], + "Attributes": [ + { + "Section": "1 Control Plane Components", + "SubSection": "1.2 API Server", + "Profile": "Level 1", + "AssessmentStatus": "Automated", + "Description": "Explicitly set a service account public key file for service accounts on the apiserver.", + "RationaleStatement": "By default, if no `--service-account-key-file` is specified to the apiserver, it uses the private key from the TLS serving certificate to verify service account tokens. To ensure that the keys for service account tokens could be rotated as needed, a separate public/private key pair should be used for signing service account tokens. Hence, the public key should be specified to the apiserver with `--service-account-key-file`.", + "ImpactStatement": "The corresponding private key must be provided to the controller manager. You would need to securely maintain the key file and rotate the keys based on your organization's key rotation policy.", + "RemediationProcedure": "Edit the API server pod specification file `/etc/kubernetes/manifests/kube-apiserver.yaml` on the Control Plane node and set the `--service-account-key-file` parameter to the public key file for service accounts: ``` --service-account-key-file= ```", + "AuditProcedure": "Run the following command on the Control Plane node: ``` ps -ef | grep kube-apiserver ``` Verify that the `--service-account-key-file` argument exists and is set as appropriate.", + "AdditionalInformation": "", + "References": "https://kubernetes.io/docs/reference/command-line-tools-reference/kube-apiserver/:https://github.com/kubernetes/kubernetes/issues/24167", + "DefaultValue": "By default, `--service-account-key-file` argument is not set." + } + ] + }, + { + "Id": "1.2.23", + "Description": "Ensure that the --etcd-certfile and --etcd-keyfile arguments are set as appropriate", + "Checks": [ + "apiserver_etcd_tls_config" + ], + "Attributes": [ + { + "Section": "1 Control Plane Components", + "SubSection": "1.2 API Server", + "Profile": "Level 1", + "AssessmentStatus": "Automated", + "Description": "etcd should be configured to make use of TLS encryption for client connections.", + "RationaleStatement": "etcd is a highly-available key value store used by Kubernetes deployments for persistent storage of all of its REST API objects. These objects are sensitive in nature and should be protected by client authentication. This requires the API server to identify itself to the etcd server using a client certificate and key.", + "ImpactStatement": "TLS and client certificate authentication must be configured for etcd.", + "RemediationProcedure": "Follow the Kubernetes documentation and set up the TLS connection between the apiserver and etcd. Then, edit the API server pod specification file `/etc/kubernetes/manifests/kube-apiserver.yaml` on the master node and set the etcd certificate and key file parameters. ``` --etcd-certfile= --etcd-keyfile= ```", + "AuditProcedure": "Run the following command on the Control Plane node: ``` ps -ef | grep kube-apiserver ``` Verify that the `--etcd-certfile` and `--etcd-keyfile` arguments exist and they are set as appropriate.", + "AdditionalInformation": "", + "References": "https://kubernetes.io/docs/reference/command-line-tools-reference/kube-apiserver/", + "DefaultValue": "By default, `--etcd-certfile` and `--etcd-keyfile` arguments are not set" + } + ] + }, + { + "Id": "1.2.24", + "Description": "Ensure that the --tls-cert-file and --tls-private-key-file arguments are set as appropriate", + "Checks": [ + "apiserver_tls_config" + ], + "Attributes": [ + { + "Section": "1 Control Plane Components", + "SubSection": "1.2 API Server", + "Profile": "Level 1", + "AssessmentStatus": "Automated", + "Description": "Setup TLS connection on the API server.", + "RationaleStatement": "API server communication contains sensitive parameters that should remain encrypted in transit. Configure the API server to serve only HTTPS traffic.", + "ImpactStatement": "TLS and client certificate authentication must be configured for your Kubernetes cluster deployment.", + "RemediationProcedure": "Follow the Kubernetes documentation and set up the TLS connection on the apiserver. Then, edit the API server pod specification file `/etc/kubernetes/manifests/kube-apiserver.yaml` on the master node and set the TLS certificate and private key file parameters. ``` --tls-cert-file= --tls-private-key-file= ```", + "AuditProcedure": "Run the following command on the Control Plane node: ``` ps -ef | grep kube-apiserver ``` Verify that the `--tls-cert-file` and `--tls-private-key-file` arguments exist and they are set as appropriate.", + "AdditionalInformation": "", + "References": "https://kubernetes.io/docs/reference/command-line-tools-reference/kube-apiserver/:https://github.com/kelseyhightower/docker-kubernetes-tls-guide", + "DefaultValue": "By default, `--tls-cert-file` and `--tls-private-key-file` are presented and created for use." + } + ] + }, + { + "Id": "1.2.25", + "Description": "Ensure that the --client-ca-file argument is set as appropriate", + "Checks": [ + "apiserver_client_ca_file_set" + ], + "Attributes": [ + { + "Section": "1 Control Plane Components", + "SubSection": "1.2 API Server", + "Profile": "Level 1", + "AssessmentStatus": "Automated", + "Description": "Setup TLS connection on the API server.", + "RationaleStatement": "API server communication contains sensitive parameters that should remain encrypted in transit. Configure the API server to serve only HTTPS traffic. If `--client-ca-file` argument is set, any request presenting a client certificate signed by one of the authorities in the `client-ca-file` is authenticated with an identity corresponding to the CommonName of the client certificate.", + "ImpactStatement": "TLS and client certificate authentication must be configured for your Kubernetes cluster deployment.", + "RemediationProcedure": "Follow the Kubernetes documentation and set up the TLS connection on the apiserver. Then, edit the API server pod specification file `/etc/kubernetes/manifests/kube-apiserver.yaml` on the master node and set the client certificate authority file. ``` --client-ca-file= ```", + "AuditProcedure": "Run the following command on the Control Plane node: ``` ps -ef | grep kube-apiserver ``` Verify that the `--client-ca-file` argument exists and it is set as appropriate.", + "AdditionalInformation": "", + "References": "https://kubernetes.io/docs/reference/command-line-tools-reference/kube-apiserver/:https://github.com/kelseyhightower/docker-kubernetes-tls-guide", + "DefaultValue": "By default, `--client-ca-file` argument is not set." + } + ] + }, + { + "Id": "1.2.26", + "Description": "Ensure that the --etcd-cafile argument is set as appropriate", + "Checks": [ + "apiserver_etcd_cafile_set" + ], + "Attributes": [ + { + "Section": "1 Control Plane Components", + "SubSection": "1.2 API Server", + "Profile": "Level 1", + "AssessmentStatus": "Automated", + "Description": "etcd should be configured to make use of TLS encryption for client connections.", + "RationaleStatement": "etcd is a highly-available key value store used by Kubernetes deployments for persistent storage of all of its REST API objects. These objects are sensitive in nature and should be protected by client authentication. This requires the API server to identify itself to the etcd server using a SSL Certificate Authority file.", + "ImpactStatement": "TLS and client certificate authentication must be configured for etcd.", + "RemediationProcedure": "Follow the Kubernetes documentation and set up the TLS connection between the apiserver and etcd. Then, edit the API server pod specification file `/etc/kubernetes/manifests/kube-apiserver.yaml` on the master node and set the etcd certificate authority file parameter. ``` --etcd-cafile= ```", + "AuditProcedure": "Run the following command on the Control Plane node: ``` ps -ef | grep kube-apiserver ``` Verify that the `--etcd-cafile` argument exists and it is set as appropriate.", + "AdditionalInformation": "", + "References": "https://kubernetes.io/docs/reference/command-line-tools-reference/kube-apiserver/", + "DefaultValue": "By default, `--etcd-cafile` is not set." + } + ] + }, + { + "Id": "1.2.27", + "Description": "Ensure that the --encryption-provider-config argument is set as appropriate", + "Checks": [ + "apiserver_encryption_provider_config_set" + ], + "Attributes": [ + { + "Section": "1 Control Plane Components", + "SubSection": "1.2 API Server", + "Profile": "Level 1", + "AssessmentStatus": "Manual", + "Description": "Encrypt etcd key-value store.", + "RationaleStatement": "etcd is a highly available key-value store used by Kubernetes deployments for persistent storage of all of its REST API objects. These objects are sensitive in nature and should be encrypted at rest to avoid any disclosures.", + "ImpactStatement": "None", + "RemediationProcedure": "Follow the Kubernetes documentation and configure a `EncryptionConfig` file. Then, edit the API server pod specification file `/etc/kubernetes/manifests/kube-apiserver.yaml` on the master node and set the `--encryption-provider-config` parameter to the path of that file: ``` --encryption-provider-config= ```", + "AuditProcedure": "Run the following command on the Control Plane node: ``` ps -ef | grep kube-apiserver ``` Verify that the `--encryption-provider-config` argument is set to a `EncryptionConfig` file. Additionally, ensure that the `EncryptionConfig` file has all the desired `resources` covered especially any secrets.", + "AdditionalInformation": "", + "References": "https://kubernetes.io/docs/tasks/administer-cluster/encrypt-data/:https://acotten.com/post/kube17-security:https://kubernetes.io/docs/reference/command-line-tools-reference/kube-apiserver/:https://github.com/kubernetes/enhancements/issues/92", + "DefaultValue": "By default, `--encryption-provider-config` is not set." + } + ] + }, + { + "Id": "1.2.28", + "Description": "Ensure that encryption providers are appropriately configured", + "Checks": [], + "Attributes": [ + { + "Section": "1 Control Plane Components", + "SubSection": "1.2 API Server", + "Profile": "Level 1", + "AssessmentStatus": "Manual", + "Description": "Where `etcd` encryption is used, appropriate providers should be configured.", + "RationaleStatement": "Where `etcd` encryption is used, it is important to ensure that the appropriate set of encryption providers is used. Currently, the `aescbc`, `kms`, and `secretbox` are likely to be appropriate options.", + "ImpactStatement": "None", + "RemediationProcedure": "Follow the Kubernetes documentation and configure a `EncryptionConfig` file. In this file, choose `aescbc`, `kms`, or `secretbox` as the encryption provider.", + "AuditProcedure": "Run the following command on the Control Plane node: ``` ps -ef | grep kube-apiserver ``` Get the `EncryptionConfig` file set for `--encryption-provider-config` argument. Verify that `aescbc`, `kms`, or `secretbox` is set as the encryption provider for all the desired `resources`.", + "AdditionalInformation": "", + "References": "https://kubernetes.io/docs/tasks/administer-cluster/encrypt-data/:https://acotten.com/post/kube17-security:https://kubernetes.io/docs/reference/command-line-tools-reference/kube-apiserver/:https://github.com/kubernetes/enhancements/issues/92:https://kubernetes.io/docs/tasks/administer-cluster/encrypt-data/#providers", + "DefaultValue": "By default, no encryption provider is set." + } + ] + }, + { + "Id": "1.2.29", + "Description": "Ensure that the API Server only makes use of Strong Cryptographic Ciphers", + "Checks": [ + "apiserver_strong_ciphers_only" + ], + "Attributes": [ + { + "Section": "1 Control Plane Components", + "SubSection": "1.2 API Server", + "Profile": "Level 1", + "AssessmentStatus": "Manual", + "Description": "Ensure that the API server is configured to only use strong cryptographic ciphers.", + "RationaleStatement": "TLS ciphers have had a number of known vulnerabilities and weaknesses, which can reduce the protection provided by them. By default Kubernetes supports a number of TLS cipher suites including some that have security concerns, weakening the protection provided.", + "ImpactStatement": "API server clients that cannot support modern cryptographic ciphers will not be able to make connections to the API server.", + "RemediationProcedure": "Edit the API server pod specification file /etc/kubernetes/manifests/kube-apiserver.yaml on the Control Plane node and set the below parameter. ``` --tls-cipher-suites=TLS_AES_128_GCM_SHA256, TLS_AES_256_GCM_SHA384, TLS_CHACHA20_POLY1305_SHA256, TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA, TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA, TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305, TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256, TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA, TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA, TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305, TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256. ```", + "AuditProcedure": "Run the following command on the Control Plane node: ``` ps -ef | grep kube-apiserver ``` Verify that the `--tls-cipher-suites` argument is set as outlined in the remediation procedure below.", + "AdditionalInformation": "Insecure values: TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256, TLS_ECDHE_ECDSA_WITH_RC4_128_SHA, TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA, TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256, TLS_ECDHE_RSA_WITH_RC4_128_SHA, TLS_RSA_WITH_3DES_EDE_CBC_SHA, TLS_RSA_WITH_AES_128_CBC_SHA, TLS_RSA_WITH_AES_128_CBC_SHA256, TLS_RSA_WITH_AES_128_GCM_SHA256, TLS_RSA_WITH_AES_256_CBC_SHA, TLS_RSA_WITH_AES_256_GCM_SHA384, TLS_RSA_WITH_RC4_128_SHA.", + "References": "https://kubernetes.io/docs/reference/command-line-tools-reference/kube-apiserver/:https://github.com/ssllabs/research/wiki/SSL-and-TLS-Deployment-Best-Practices#23-use-secure-cipher-suites", + "DefaultValue": "By default the Kubernetes API server supports a wide range of TLS ciphers" + } + ] + }, + { + "Id": "1.2.30", + "Description": "Ensure that the --service-account-extend-token-expiration parameter is set to false", + "Checks": [], + "Attributes": [ + { + "Section": "1 Control Plane Components", + "SubSection": "1.2 API Server", + "Profile": "Level 1", + "AssessmentStatus": "Automated", + "Description": "By default Kubernetes extends service account token lifetimes to one year to aid in transition from the legacy token settings.", + "RationaleStatement": "This default setting is not ideal for security as it ignores other settings related to maximum token lifetime and means that a lost or stolen credential could be valid for an extended period of time.", + "ImpactStatement": "Disabling this setting means that the service account token expiry set in the cluster will be enforced, and service account tokens will expire at the end of that time frame.", + "RemediationProcedure": "Edit the API server pod specification file /etc/kubernetes/manifests/kube-apiserver.yaml on the Control Plane node and set the --service-account-extend-token-expiration parameter to false. ``` --service-account-extend-token-expiration=false ```", + "AuditProcedure": "Run the following command on the Control Plane node: ``` ps -ef | grep kube-apiserver ``` Verify that the --service-account-extend-token-expiration argument is set to false.", + "AdditionalInformation": "", + "References": "https://kubernetes.io/docs/reference/command-line-tools-reference/kube-apiserver/", + "DefaultValue": "By default, this parameter is set to true" + } + ] + }, + { + "Id": "1.3.1", + "Description": "Ensure that the --terminated-pod-gc-threshold argument is set as appropriate", + "Checks": [ + "controllermanager_garbage_collection" + ], + "Attributes": [ + { + "Section": "1 Control Plane Components", + "SubSection": "1.3 Controller Manager", + "Profile": "Level 1", + "AssessmentStatus": "Manual", + "Description": "Activate garbage collector on pod termination, as appropriate.", + "RationaleStatement": "Garbage collection is important to ensure sufficient resource availability and avoiding degraded performance and availability. In the worst case, the system might crash or just be unusable for a long period of time. The current setting for garbage collection is 12,500 terminated pods which might be too high for your system to sustain. Based on your system resources and tests, choose an appropriate threshold value to activate garbage collection.", + "ImpactStatement": "None", + "RemediationProcedure": "Edit the Controller Manager pod specification file `/etc/kubernetes/manifests/kube-controller-manager.yaml` on the Control Plane node and set the `--terminated-pod-gc-threshold` to an appropriate threshold, for example: ``` --terminated-pod-gc-threshold=10 ```", + "AuditProcedure": "Run the following command on the Control Plane node: ``` ps -ef | grep kube-controller-manager ``` Verify that the `--terminated-pod-gc-threshold` argument is set as appropriate.", + "AdditionalInformation": "", + "References": "https://kubernetes.io/docs/admin/kube-controller-manager/:https://github.com/kubernetes/kubernetes/issues/28484", + "DefaultValue": "By default, `--terminated-pod-gc-threshold` is set to `12500`." + } + ] + }, + { + "Id": "1.3.2", + "Description": "Ensure that the --profiling argument is set to false", + "Checks": [ + "controllermanager_disable_profiling" + ], + "Attributes": [ + { + "Section": "1 Control Plane Components", + "SubSection": "1.3 Controller Manager", + "Profile": "Level 1", + "AssessmentStatus": "Automated", + "Description": "Disable profiling, if not needed.", + "RationaleStatement": "Profiling allows for the identification of specific performance bottlenecks. It generates a significant amount of program data that could potentially be exploited to uncover system and program details. If you are not experiencing any bottlenecks and do not need the profiler for troubleshooting purposes, it is recommended to turn it off to reduce the potential attack surface.", + "ImpactStatement": "Profiling information would not be available.", + "RemediationProcedure": "Edit the Controller Manager pod specification file `/etc/kubernetes/manifests/kube-controller-manager.yaml` on the Control Plane node and set the below parameter. ``` --profiling=false ```", + "AuditProcedure": "Run the following command on the Control Plane node: ``` ps -ef | grep kube-controller-manager ``` Verify that the `--profiling` argument is set to `false`.", + "AdditionalInformation": "", + "References": "https://kubernetes.io/docs/admin/kube-controller-manager/:https://github.com/kubernetes/community/blob/master/contributors/devel/profiling.md", + "DefaultValue": "By default, profiling is enabled." + } + ] + }, + { + "Id": "1.3.3", + "Description": "Ensure that the --use-service-account-credentials argument is set to true", + "Checks": [ + "controllermanager_service_account_credentials" + ], + "Attributes": [ + { + "Section": "1 Control Plane Components", + "SubSection": "1.3 Controller Manager", + "Profile": "Level 1", + "AssessmentStatus": "Automated", + "Description": "Use individual service account credentials for each controller.", + "RationaleStatement": "The controller manager creates a service account per controller in the `kube-system` namespace, generates a credential for it, and builds a dedicated API client with that service account credential for each controller loop to use. Setting the `--use-service-account-credentials` to `true` runs each control loop within the controller manager using a separate service account credential. When used in combination with RBAC, this ensures that the control loops run with the minimum permissions required to perform their intended tasks.", + "ImpactStatement": "Whatever authorizer is configured for the cluster, it must grant sufficient permissions to the service accounts to perform their intended tasks. When using the RBAC authorizer, those roles are created and bound to the appropriate service accounts in the `kube-system` namespace automatically with default roles and rolebindings that are auto-reconciled on startup. If using other authorization methods (ABAC, Webhook, etc), the cluster deployer is responsible for granting appropriate permissions to the service accounts (the required permissions can be seen by inspecting the `controller-roles.yaml` and `controller-role-bindings.yaml` files for the RBAC roles.", + "RemediationProcedure": "Edit the Controller Manager pod specification file `/etc/kubernetes/manifests/kube-controller-manager.yaml` on the Control Plane node to set the below parameter. ``` --use-service-account-credentials=true ```", + "AuditProcedure": "Run the following command on the Control Plane node: ``` ps -ef | grep kube-controller-manager ``` Verify that the `--use-service-account-credentials` argument is set to `true`.", + "AdditionalInformation": "", + "References": "https://kubernetes.io/docs/admin/kube-controller-manager/:https://kubernetes.io/docs/admin/service-accounts-admin/:https://github.com/kubernetes/kubernetes/blob/release-1.6/plugin/pkg/auth/authorizer/rbac/bootstrappolicy/testdata/controller-roles.yaml:https://github.com/kubernetes/kubernetes/blob/release-1.6/plugin/pkg/auth/authorizer/rbac/bootstrappolicy/testdata/controller-role-bindings.yaml:https://kubernetes.io/docs/admin/authorization/rbac/#controller-roles", + "DefaultValue": "By default, `--use-service-account-credentials` is set to false." + } + ] + }, + { + "Id": "1.3.4", + "Description": "Ensure that the --service-account-private-key-file argument is set as appropriate", + "Checks": [ + "controllermanager_service_account_private_key_file" + ], + "Attributes": [ + { + "Section": "1 Control Plane Components", + "SubSection": "1.3 Controller Manager", + "Profile": "Level 1", + "AssessmentStatus": "Automated", + "Description": "Explicitly set a service account private key file for service accounts on the controller manager.", + "RationaleStatement": "To ensure that keys for service account tokens can be rotated as needed, a separate public/private key pair should be used for signing service account tokens. The private key should be specified to the controller manager with `--service-account-private-key-file` as appropriate.", + "ImpactStatement": "You would need to securely maintain the key file and rotate the keys based on your organization's key rotation policy.", + "RemediationProcedure": "Edit the Controller Manager pod specification file `/etc/kubernetes/manifests/kube-controller-manager.yaml` on the Control Plane node and set the `--service-account-private-key-file` parameter to the private key file for service accounts. ``` --service-account-private-key-file= ```", + "AuditProcedure": "Run the following command on the Control Plane node: ``` ps -ef | grep kube-controller-manager ``` Verify that the `--service-account-private-key-file` argument is set as appropriate.", + "AdditionalInformation": "", + "References": "https://kubernetes.io/docs/admin/kube-controller-manager/", + "DefaultValue": "By default, `--service-account-private-key-file` it not set." + } + ] + }, + { + "Id": "1.3.5", + "Description": "Ensure that the --root-ca-file argument is set as appropriate", + "Checks": [ + "controllermanager_root_ca_file_set" + ], + "Attributes": [ + { + "Section": "1 Control Plane Components", + "SubSection": "1.3 Controller Manager", + "Profile": "Level 1", + "AssessmentStatus": "Automated", + "Description": "Allow pods to verify the API server's serving certificate before establishing connections.", + "RationaleStatement": "Processes running within pods that need to contact the API server must verify the API server's serving certificate. Failing to do so could be a subject to man-in-the-middle attacks. Providing the root certificate for the API server's serving certificate to the controller manager with the `--root-ca-file` argument allows the controller manager to inject the trusted bundle into pods so that they can verify TLS connections to the API server.", + "ImpactStatement": "You need to setup and maintain root certificate authority file.", + "RemediationProcedure": "Edit the Controller Manager pod specification file `/etc/kubernetes/manifests/kube-controller-manager.yaml` on the Control Plane node and set the `--root-ca-file` parameter to the certificate bundle file`. ``` --root-ca-file= ```", + "AuditProcedure": "Run the following command on the Control Plane node: ``` ps -ef | grep kube-controller-manager ``` Verify that the `--root-ca-file` argument exists and is set to a certificate bundle file containing the root certificate for the API server's serving certificate.", + "AdditionalInformation": "", + "References": "https://kubernetes.io/docs/admin/kube-controller-manager/:https://github.com/kubernetes/kubernetes/issues/11000", + "DefaultValue": "By default, `--root-ca-file` is not set." + } + ] + }, + { + "Id": "1.3.6", + "Description": "Ensure that the RotateKubeletServerCertificate argument is set to true", + "Checks": [ + "controllermanager_rotate_kubelet_server_cert" + ], + "Attributes": [ + { + "Section": "1 Control Plane Components", + "SubSection": "1.3 Controller Manager", + "Profile": "Level 1", + "AssessmentStatus": "Automated", + "Description": "Enable kubelet server certificate rotation on controller-manager.", + "RationaleStatement": "`RotateKubeletServerCertificate` causes the kubelet to both request a serving certificate after bootstrapping its client credentials and rotate the certificate as its existing credentials expire. This automated periodic rotation ensures that the there are no downtimes due to expired certificates and thus addressing availability in the CIA security triad. Note: This recommendation only applies if you let kubelets get their certificates from the API server. In case your kubelet certificates come from an outside authority/tool (e.g. Vault) then you need to take care of rotation yourself.", + "ImpactStatement": "None", + "RemediationProcedure": "Edit the Controller Manager pod specification file `/etc/kubernetes/manifests/kube-controller-manager.yaml` on the Control Plane node and set the `--feature-gates` parameter to include `RotateKubeletServerCertificate=true`. ``` --feature-gates=RotateKubeletServerCertificate=true ```", + "AuditProcedure": "Run the following command on the Control Plane node: ``` ps -ef | grep kube-controller-manager ``` Verify that `RotateKubeletServerCertificate` argument exists and is set to `true`.", + "AdditionalInformation": "", + "References": "https://kubernetes.io/docs/admin/kubelet-tls-bootstrapping/#approval-controller:https://github.com/kubernetes/features/issues/267:https://github.com/kubernetes/kubernetes/pull/45059:https://kubernetes.io/docs/admin/kube-controller-manager/", + "DefaultValue": "By default, `RotateKubeletServerCertificate` is set to true this recommendation verifies that it has not been disabled." + } + ] + }, + { + "Id": "1.3.7", + "Description": "Ensure that the --bind-address argument is set to 127.0.0.1", + "Checks": [ + "controllermanager_bind_address" + ], + "Attributes": [ + { + "Section": "1 Control Plane Components", + "SubSection": "1.3 Controller Manager", + "Profile": "Level 1", + "AssessmentStatus": "Automated", + "Description": "Do not bind the Controller Manager service to non-loopback insecure addresses.", + "RationaleStatement": "The Controller Manager API service which runs on port 10252/TCP by default is used for health and metrics information and is available without authentication or encryption. As such it should only be bound to a localhost interface, to minimize the cluster's attack surface", + "ImpactStatement": "None", + "RemediationProcedure": "Edit the Controller Manager pod specification file `/etc/kubernetes/manifests/kube-controller-manager.yaml` on the Control Plane node and ensure the correct value for the `--bind-address` parameter", + "AuditProcedure": "Run the following command on the Control Plane node: ``` ps -ef | grep kube-controller-manager ``` Verify that the `--bind-address` argument is set to 127.0.0.1", + "AdditionalInformation": "Although the current Kubernetes documentation site says that `--address` is deprecated in favour of `--bind-address` Kubeadm 1.11 still makes use of `--address`", + "References": "https://kubernetes.io/docs/reference/command-line-tools-reference/kube-controller-manager/", + "DefaultValue": "By default, the `--bind-address` parameter is set to 0.0.0.0" + } + ] + }, + { + "Id": "1.4.1", + "Description": "Ensure that the --profiling argument is set to false", + "Checks": [ + "scheduler_profiling" + ], + "Attributes": [ + { + "Section": "1 Control Plane Components", + "SubSection": "1.4 Scheduler", + "Profile": "Level 1", + "AssessmentStatus": "Automated", + "Description": "Disable profiling, if not needed.", + "RationaleStatement": "Profiling allows for the identification of specific performance bottlenecks. It generates a significant amount of program data that could potentially be exploited to uncover system and program details. If you are not experiencing any bottlenecks and do not need the profiler for troubleshooting purposes, it is recommended to turn it off to reduce the potential attack surface.", + "ImpactStatement": "Profiling information would not be available.", + "RemediationProcedure": "Edit the Scheduler pod specification file `/etc/kubernetes/manifests/kube-scheduler.yaml` file on the Control Plane node and set the below parameter. ``` --profiling=false ```", + "AuditProcedure": "Run the following command on the Control Plane node: ``` ps -ef | grep kube-scheduler ``` Verify that the `--profiling` argument is set to `false`.", + "AdditionalInformation": "", + "References": "https://kubernetes.io/docs/admin/kube-scheduler/:https://github.com/kubernetes/community/blob/master/contributors/devel/profiling.md", + "DefaultValue": "By default, profiling is enabled." + } + ] + }, + { + "Id": "1.4.2", + "Description": "Ensure that the --bind-address argument is set to 127.0.0.1", + "Checks": [ + "scheduler_bind_address" + ], + "Attributes": [ + { + "Section": "1 Control Plane Components", + "SubSection": "1.4 Scheduler", + "Profile": "Level 1", + "AssessmentStatus": "Automated", + "Description": "Do not bind the scheduler service to non-loopback insecure addresses.", + "RationaleStatement": "The Scheduler API service which runs on port 10251/TCP by default is used for health and metrics information and is available without authentication or encryption. As such it should only be bound to a localhost interface, to minimize the cluster's attack surface", + "ImpactStatement": "None", + "RemediationProcedure": "Edit the Scheduler pod specification file `/etc/kubernetes/manifests/kube-scheduler.yaml` on the Control Plane node and ensure the correct value for the `--bind-address` parameter", + "AuditProcedure": "Run the following command on the Control Plane node: ``` ps -ef | grep kube-scheduler ``` Verify that the `--bind-address` argument is set to 127.0.0.1", + "AdditionalInformation": "", + "References": "https://kubernetes.io/docs/reference/command-line-tools-reference/kube-scheduler/", + "DefaultValue": "By default, the `--bind-address` parameter is set to 0.0.0.0" + } + ] + }, + { + "Id": "2.1", + "Description": "Ensure that the --cert-file and --key-file arguments are set as appropriate", + "Checks": [ + "etcd_tls_encryption" + ], + "Attributes": [ + { + "Section": "2 etcd", + "Profile": "Level 1", + "AssessmentStatus": "Automated", + "Description": "Configure TLS encryption for the etcd service.", + "RationaleStatement": "etcd is a highly-available key value store used by Kubernetes deployments for persistent storage of all of its REST API objects. These objects are sensitive in nature and should be encrypted in transit.", + "ImpactStatement": "Client connections only over TLS would be served.", + "RemediationProcedure": "Follow the etcd service documentation and configure TLS encryption. Then, edit the etcd pod specification file `/etc/kubernetes/manifests/etcd.yaml` on the master node and set the below parameters. ``` --cert-file= --key-file= ```", + "AuditProcedure": "Run the following command on the etcd server node ``` ps -ef | grep etcd ``` Verify that the `--cert-file` and the `--key-file` arguments are set as appropriate.", + "AdditionalInformation": "", + "References": "https://coreos.com/etcd/docs/latest/op-guide/security.html:https://kubernetes.io/docs/admin/etcd/", + "DefaultValue": "By default, TLS encryption is not set." + } + ] + }, + { + "Id": "2.2", + "Description": "Ensure that the --client-cert-auth argument is set to true", + "Checks": [ + "etcd_client_cert_auth" + ], + "Attributes": [ + { + "Section": "2 etcd", + "Profile": "Level 1", + "AssessmentStatus": "Automated", + "Description": "Enable client authentication on etcd service.", + "RationaleStatement": "etcd is a highly-available key value store used by Kubernetes deployments for persistent storage of all of its REST API objects. These objects are sensitive in nature and should not be available to unauthenticated clients. You should enable the client authentication via valid certificates to secure the access to the etcd service.", + "ImpactStatement": "All clients attempting to access the etcd server will require a valid client certificate.", + "RemediationProcedure": "Edit the etcd pod specification file `/etc/kubernetes/manifests/etcd.yaml` on the master node and set the below parameter. ``` --client-cert-auth=true ```", + "AuditProcedure": "Run the following command on the etcd server node: ``` ps -ef | grep etcd ``` Verify that the `--client-cert-auth` argument is set to `true`.", + "AdditionalInformation": "", + "References": "https://coreos.com/etcd/docs/latest/op-guide/security.html:https://kubernetes.io/docs/admin/etcd/:https://coreos.com/etcd/docs/latest/op-guide/configuration.html#client-cert-auth", + "DefaultValue": "By default, the etcd service can be queried by unauthenticated clients." + } + ] + }, + { + "Id": "2.3", + "Description": "Ensure that the --auto-tls argument is not set to true", + "Checks": [ + "etcd_no_auto_tls" + ], + "Attributes": [ + { + "Section": "2 etcd", + "Profile": "Level 1", + "AssessmentStatus": "Automated", + "Description": "Do not use self-signed certificates for TLS.", + "RationaleStatement": "etcd is a highly-available key value store used by Kubernetes deployments for persistent storage of all of its REST API objects. These objects are sensitive in nature and should not be available to unauthenticated clients. You should enable the client authentication via valid certificates to secure the access to the etcd service.", + "ImpactStatement": "Clients will not be able to use self-signed certificates for TLS.", + "RemediationProcedure": "Edit the etcd pod specification file `/etc/kubernetes/manifests/etcd.yaml` on the master node and either remove the `--auto-tls` parameter or set it to `false`. ``` --auto-tls=false ```", + "AuditProcedure": "Run the following command on the etcd server node: ``` ps -ef | grep etcd ``` Verify that if the `--auto-tls` argument exists, it is not set to `true`.", + "AdditionalInformation": "", + "References": "https://coreos.com/etcd/docs/latest/op-guide/security.html:https://kubernetes.io/docs/admin/etcd/:https://coreos.com/etcd/docs/latest/op-guide/configuration.html#auto-tls", + "DefaultValue": "By default, `--auto-tls` is set to `false`." + } + ] + }, + { + "Id": "2.4", + "Description": "Ensure that the --peer-cert-file and --peer-key-file arguments are set as appropriate", + "Checks": [ + "etcd_peer_tls_config" + ], + "Attributes": [ + { + "Section": "2 etcd", + "Profile": "Level 1", + "AssessmentStatus": "Automated", + "Description": "etcd should be configured to make use of TLS encryption for peer connections.", + "RationaleStatement": "etcd is a highly-available key value store used by Kubernetes deployments for persistent storage of all of its REST API objects. These objects are sensitive in nature and should be encrypted in transit and also amongst peers in the etcd clusters.", + "ImpactStatement": "etcd cluster peers would need to set up TLS for their communication.", + "RemediationProcedure": "Follow the etcd service documentation and configure peer TLS encryption as appropriate for your etcd cluster. Then, edit the etcd pod specification file `/etc/kubernetes/manifests/etcd.yaml` on the master node and set the below parameters. ``` --peer-client-file= --peer-key-file= ```", + "AuditProcedure": "Run the following command on the etcd server node: ``` ps -ef | grep etcd ``` Verify that the `--peer-cert-file` and `--peer-key-file` arguments are set as appropriate. **Note:** This recommendation is applicable only for etcd clusters. If you are using only one etcd server in your environment then this recommendation is not applicable.", + "AdditionalInformation": "", + "References": "https://coreos.com/etcd/docs/latest/op-guide/security.html:https://kubernetes.io/docs/admin/etcd/", + "DefaultValue": "**Note:** This recommendation is applicable only for etcd clusters. If you are using only one etcd server in your environment then this recommendation is not applicable. By default, peer communication over TLS is not configured." + } + ] + }, + { + "Id": "2.5", + "Description": "Ensure that the --peer-client-cert-auth argument is set to true", + "Checks": [ + "etcd_peer_client_cert_auth" + ], + "Attributes": [ + { + "Section": "2 etcd", + "Profile": "Level 1", + "AssessmentStatus": "Automated", + "Description": "etcd should be configured for peer authentication.", + "RationaleStatement": "etcd is a highly-available key value store used by Kubernetes deployments for persistent storage of all of its REST API objects. These objects are sensitive in nature and should be accessible only by authenticated etcd peers in the etcd cluster.", + "ImpactStatement": "All peers attempting to communicate with the etcd server will require a valid client certificate for authentication.", + "RemediationProcedure": "Edit the etcd pod specification file `/etc/kubernetes/manifests/etcd.yaml` on the master node and set the below parameter. ``` --peer-client-cert-auth=true ```", + "AuditProcedure": "Run the following command on the etcd server node: ``` ps -ef | grep etcd ``` Verify that the `--peer-client-cert-auth` argument is set to `true`. **Note:** This recommendation is applicable only for etcd clusters. If you are using only one etcd server in your environment then this recommendation is not applicable.", + "AdditionalInformation": "", + "References": "https://coreos.com/etcd/docs/latest/op-guide/security.html:https://kubernetes.io/docs/admin/etcd/:https://coreos.com/etcd/docs/latest/op-guide/configuration.html#peer-client-cert-auth", + "DefaultValue": "**Note:** This recommendation is applicable only for etcd clusters. If you are using only one etcd server in your environment then this recommendation is not applicable. By default, `--peer-client-cert-auth` argument is set to `false`." + } + ] + }, + { + "Id": "2.6", + "Description": "Ensure that the --peer-auto-tls argument is not set to true", + "Checks": [ + "etcd_no_peer_auto_tls" + ], + "Attributes": [ + { + "Section": "2 etcd", + "Profile": "Level 1", + "AssessmentStatus": "Automated", + "Description": "Do not use automatically generated self-signed certificates for TLS connections between peers.", + "RationaleStatement": "etcd is a highly-available key value store used by Kubernetes deployments for persistent storage of all of its REST API objects. These objects are sensitive in nature and should be accessible only by authenticated etcd peers in the etcd cluster. Hence, do not use self-signed certificates for authentication.", + "ImpactStatement": "All peers attempting to communicate with the etcd server will require a valid client certificate for authentication.", + "RemediationProcedure": "Edit the etcd pod specification file `/etc/kubernetes/manifests/etcd.yaml` on the master node and either remove the `--peer-auto-tls` parameter or set it to `false`. ``` --peer-auto-tls=false ```", + "AuditProcedure": "Run the following command on the etcd server node: ``` ps -ef | grep etcd ``` Verify that if the `--peer-auto-tls` argument exists, it is not set to `true`. **Note:** This recommendation is applicable only for etcd clusters. If you are using only one etcd server in your environment then this recommendation is not applicable.", + "AdditionalInformation": "", + "References": "https://coreos.com/etcd/docs/latest/op-guide/security.html:https://kubernetes.io/docs/admin/etcd/:https://coreos.com/etcd/docs/latest/op-guide/configuration.html#peer-auto-tls", + "DefaultValue": "**Note:** This recommendation is applicable only for etcd clusters. If you are using only one etcd server in your environment then this recommendation is not applicable. By default, `--peer-auto-tls` argument is set to `false`." + } + ] + }, + { + "Id": "2.7", + "Description": "Ensure that a unique Certificate Authority is used for etcd", + "Checks": [ + "etcd_unique_ca" + ], + "Attributes": [ + { + "Section": "2 etcd", + "Profile": "Level 2", + "AssessmentStatus": "Manual", + "Description": "Use a different certificate authority for etcd from the one used for Kubernetes.", + "RationaleStatement": "etcd is a highly available key-value store used by Kubernetes deployments for persistent storage of all of its REST API objects. Its access should be restricted to specifically designated clients and peers only. Authentication to etcd is based on whether the certificate presented was issued by a trusted certificate authority. There is no checking of certificate attributes such as common name or subject alternative name. As such, if any attackers were able to gain access to any certificate issued by the trusted certificate authority, they would be able to gain full access to the etcd database.", + "ImpactStatement": "Additional management of the certificates and keys for the dedicated certificate authority will be required.", + "RemediationProcedure": "Follow the etcd documentation and create a dedicated certificate authority setup for the etcd service. Then, edit the etcd pod specification file `/etc/kubernetes/manifests/etcd.yaml` on the master node and set the below parameter. ``` --trusted-ca-file= ```", + "AuditProcedure": "Review the CA used by the etcd environment and ensure that it does not match the CA certificate file used for the management of the overall Kubernetes cluster. Run the following command on the master node: ``` ps -ef | grep etcd ``` Note the file referenced by the `--trusted-ca-file` argument. Run the following command on the master node: ``` ps -ef | grep apiserver ``` Verify that the file referenced by the `--client-ca-file` for apiserver is different from the `--trusted-ca-file` used by etcd.", + "AdditionalInformation": "", + "References": "https://coreos.com/etcd/docs/latest/op-guide/security.html", + "DefaultValue": "By default, no etcd certificate is created and used." + } + ] + }, + { + "Id": "3.1.1", + "Description": "Client certificate authentication should not be used for users", + "Checks": [], + "Attributes": [ + { + "Section": "3 Control Plane Configuration", + "SubSection": "3.1 Authentication and Authorization", + "Profile": "Level 1", + "AssessmentStatus": "Manual", + "Description": "Kubernetes provides the option to use client certificates for user authentication. However as there is no way to revoke these certificates when a user leaves an organization or loses their credential, they are not suitable for this purpose. It is not possible to fully disable client certificate use within a cluster as it is used for component to component authentication.", + "RationaleStatement": "With any authentication mechanism the ability to revoke credentials if they are compromised or no longer required, is a key control. Kubernetes client certificate authentication does not allow for this due to a lack of support for certificate revocation.", + "ImpactStatement": "External mechanisms for authentication generally require additional software to be deployed.", + "RemediationProcedure": "Alternative mechanisms provided by Kubernetes such as the use of OIDC should be implemented in place of client certificates.", + "AuditProcedure": "Review user access to the cluster and ensure that users are not making use of Kubernetes client certificate authentication.", + "AdditionalInformation": "The lack of certificate revocation was flagged up as a high risk issue in the recent Kubernetes security audit. Without this feature, client certificate authentication is not suitable for end users.", + "References": "", + "DefaultValue": "Client certificate authentication is enabled by default." + } + ] + }, + { + "Id": "3.1.2", + "Description": "Service account token authentication should not be used for users", + "Checks": [], + "Attributes": [ + { + "Section": "3 Control Plane Configuration", + "SubSection": "3.1 Authentication and Authorization", + "Profile": "Level 1", + "AssessmentStatus": "Manual", + "Description": "Kubernetes provides service account tokens which are intended for use by workloads running in the Kubernetes cluster, for authentication to the API server. These tokens are not designed for use by end-users and do not provide for features such as revocation or expiry, making them insecure. A newer version of the feature (Bound service account token volumes) does introduce expiry but still does not allow for specific revocation.", + "RationaleStatement": "With any authentication mechanism the ability to revoke credentials if they are compromised or no longer required, is a key control. Service account token authentication does not allow for this due to the use of JWT tokens as an underlying technology.", + "ImpactStatement": "External mechanisms for authentication generally require additional software to be deployed.", + "RemediationProcedure": "Alternative mechanisms provided by Kubernetes such as the use of OIDC should be implemented in place of service account tokens.", + "AuditProcedure": "Review user access to the cluster and ensure that users are not making use of service account token authentication.", + "AdditionalInformation": "", + "References": "", + "DefaultValue": "Service account token authentication is enabled by default." + } + ] + }, + { + "Id": "3.1.3", + "Description": "Bootstrap token authentication should not be used for users", + "Checks": [], + "Attributes": [ + { + "Section": "3 Control Plane Configuration", + "SubSection": "3.1 Authentication and Authorization", + "Profile": "Level 1", + "AssessmentStatus": "Manual", + "Description": "Kubernetes provides bootstrap tokens which are intended for use by new nodes joining the cluster These tokens are not designed for use by end-users they are specifically designed for the purpose of bootstrapping new nodes and not for general authentication", + "RationaleStatement": "Bootstrap tokens are not intended for use as a general authentication mechanism and impose constraints on user and group naming that do not facilitate good RBAC design. They also cannot be used with MFA resulting in a weak authentication mechanism being available.", + "ImpactStatement": "External mechanisms for authentication generally require additional software to be deployed.", + "RemediationProcedure": "Alternative mechanisms provided by Kubernetes such as the use of OIDC should be implemented in place of bootstrap tokens.", + "AuditProcedure": "Review user access to the cluster and ensure that users are not making use of bootstrap token authentication.", + "AdditionalInformation": "", + "References": "", + "DefaultValue": "Bootstrap token authentication is not enabled by default and requires an API server parameter to be set." + } + ] + }, + { + "Id": "3.2.1", + "Description": "Ensure that a minimal audit policy is created", + "Checks": [], + "Attributes": [ + { + "Section": "3 Control Plane Configuration", + "SubSection": "3.2 Logging", + "Profile": "Level 1", + "AssessmentStatus": "Manual", + "Description": "Kubernetes can audit the details of requests made to the API server. The `--audit-policy-file` flag must be set for this logging to be enabled.", + "RationaleStatement": "Logging is an important detective control for all systems, to detect potential unauthorised access.", + "ImpactStatement": "Audit logs will be created on the master nodes, which will consume disk space. Care should be taken to avoid generating too large volumes of log information as this could impact the available of the cluster nodes.", + "RemediationProcedure": "Create an audit policy file for your cluster.", + "AuditProcedure": "Run the following command on one of the cluster master nodes: ``` ps -ef | grep kube-apiserver ``` Verify that the `--audit-policy-file` is set. Review the contents of the file specified and ensure that it contains a valid audit policy.", + "AdditionalInformation": "", + "References": "https://kubernetes.io/docs/tasks/debug-application-cluster/audit/", + "DefaultValue": "Unless the `--audit-policy-file` flag is specified, no auditing will be carried out." + } + ] + }, + { + "Id": "3.2.2", + "Description": "Ensure that the audit policy covers key security concerns", + "Checks": [], + "Attributes": [ + { + "Section": "3 Control Plane Configuration", + "SubSection": "3.2 Logging", + "Profile": "Level 2", + "AssessmentStatus": "Manual", + "Description": "Ensure that the audit policy created for the cluster covers key security concerns.", + "RationaleStatement": "Security audit logs should cover access and modification of key resources in the cluster, to enable them to form an effective part of a security environment.", + "ImpactStatement": "Increasing audit logging will consume resources on the nodes or other log destination.", + "RemediationProcedure": "Consider modification of the audit policy in use on the cluster to include these items, at a minimum.", + "AuditProcedure": "Review the audit policy provided for the cluster and ensure that it covers at least the following areas :- - Access to Secrets managed by the cluster. Care should be taken to only log Metadata for requests to Secrets, ConfigMaps, and TokenReviews, in order to avoid the risk of logging sensitive data. - Modification of `pod` and `deployment` objects. - Use of `pods/exec`, `pods/portforward`, `pods/proxy` and `services/proxy`. For most requests, minimally logging at the Metadata level is recommended (the most basic level of logging).", + "AdditionalInformation": "", + "References": "https://github.com/k8scop/k8s-security-dashboard/blob/master/configs/kubernetes/adv-audit.yaml:https://kubernetes.io/docs/tasks/debug-application-cluster/audit/#audit-policy:https://github.com/kubernetes/kubernetes/blob/master/cluster/gce/gci/configure-helper.sh#L735", + "DefaultValue": "By default Kubernetes clusters do not log audit information." + } + ] + }, + { + "Id": "4.1.1", + "Description": "Ensure that the kubelet service file permissions are set to 600 or more restrictive", + "Checks": [ + "kubelet_service_file_permissions" + ], + "Attributes": [ + { + "Section": "4 Worker Nodes", + "SubSection": "4.1 Worker Node Configuration Files", + "Profile": "Level 1", + "AssessmentStatus": "Automated", + "Description": "Ensure that the `kubelet` service file has permissions of `600` or more restrictive.", + "RationaleStatement": "The `kubelet` service file controls various parameters that set the behavior of the `kubelet` service in the worker node. You should restrict its file permissions to maintain the integrity of the file. The file should be writable by only the administrators on the system.", + "ImpactStatement": "None", + "RemediationProcedure": "Run the below command (based on the file location on your system) on the each worker node. For example, ``` chmod 600 /etc/systemd/system/kubelet.service.d/kubeadm.conf ```", + "AuditProcedure": "Automated AAC auditing has been modified to allow CIS-CAT to input a variable for the / of the kubelet service config file. Please set $kubelet_service_config= based on the file location on your system for example: ``` export kubelet_service_config=/etc/systemd/system/kubelet.service.d/kubeadm.conf ``` To perform the audit manually: Run the below command (based on the file location on your system) on the each worker node. For example, ``` stat -c %a /etc/systemd/system/kubelet.service.d/10-kubeadm.conf ``` Verify that the permissions are `600` or more restrictive.", + "AdditionalInformation": "", + "References": "https://kubernetes.io/docs/admin/kubelet/:https://kubernetes.io/docs/setup/independent/create-cluster-kubeadm/#44-joining-your-nodes:https://kubernetes.io/docs/admin/kubeadm/#kubelet-drop-in", + "DefaultValue": "By default, the `kubelet` service file has permissions of `640`." + } + ] + }, + { + "Id": "4.1.2", + "Description": "Ensure that the kubelet service file ownership is set to root:root", + "Checks": [ + "kubelet_service_file_ownership_root" + ], + "Attributes": [ + { + "Section": "4 Worker Nodes", + "SubSection": "4.1 Worker Node Configuration Files", + "Profile": "Level 1", + "AssessmentStatus": "Automated", + "Description": "Ensure that the `kubelet` service file ownership is set to `root:root`.", + "RationaleStatement": "The `kubelet` service file controls various parameters that set the behavior of the `kubelet` service in the worker node. You should set its file ownership to maintain the integrity of the file. The file should be owned by `root:root`.", + "ImpactStatement": "None", + "RemediationProcedure": "Run the below command (based on the file location on your system) on the each worker node. For example, ``` chown root:root /etc/systemd/system/kubelet.service.d/kubeadm.conf ```", + "AuditProcedure": "Automated AAC auditing has been modified to allow CIS-CAT to input a variable for the / of the kubelet service config file. Please set $kubelet_service_config= based on the file location on your system for example: ``` export kubelet_service_config=/etc/systemd/system/kubelet.service.d/kubeadm.conf ``` To perform the audit manually: Run the below command (based on the file location on your system) on the each worker node. For example, ``` stat -c %U:%G /etc/systemd/system/kubelet.service.d/10-kubeadm.conf ``` Verify that the ownership is set to `root:root`.", + "AdditionalInformation": "", + "References": "https://kubernetes.io/docs/admin/kubelet/:https://kubernetes.io/docs/setup/independent/create-cluster-kubeadm/#44-joining-your-nodes:https://kubernetes.io/docs/admin/kubeadm/#kubelet-drop-in", + "DefaultValue": "By default, `kubelet` service file ownership is set to `root:root`." + } + ] + }, + { + "Id": "4.1.3", + "Description": "If proxy kubeconfig file exists ensure permissions are set to 600 or more restrictive", + "Checks": [], + "Attributes": [ + { + "Section": "4 Worker Nodes", + "SubSection": "4.1 Worker Node Configuration Files", + "Profile": "Level 1", + "AssessmentStatus": "Manual", + "Description": "If `kube-proxy` is running, and if it is using a file-based kubeconfig file, ensure that the proxy kubeconfig file has permissions of `600` or more restrictive.", + "RationaleStatement": "The `kube-proxy` kubeconfig file controls various parameters of the `kube-proxy` service in the worker node. You should restrict its file permissions to maintain the integrity of the file. The file should be writable by only the administrators on the system. It is possible to run `kube-proxy` with the kubeconfig parameters configured as a Kubernetes ConfigMap instead of a file. In this case, there is no proxy kubeconfig file.", + "ImpactStatement": "None", + "RemediationProcedure": "Run the below command (based on the file location on your system) on the each worker node. For example, ``` chmod 600 ```", + "AuditProcedure": "Find the kubeconfig file being used by `kube-proxy` by running the following command: ``` ps -ef | grep kube-proxy ``` If `kube-proxy` is running, get the kubeconfig file location from the `--kubeconfig` parameter. To perform the audit: Run the below command (based on the file location on your system) on the each worker node. For example, ``` stat -c %a ``` Verify that a file is specified and it exists with permissions are `600` or more restrictive.", + "AdditionalInformation": "", + "References": "https://kubernetes.io/docs/admin/kube-proxy/", + "DefaultValue": "By default, proxy file has permissions of `640`." + } + ] + }, + { + "Id": "4.1.4", + "Description": "If proxy kubeconfig file exists ensure ownership is set to root:root", + "Checks": [], + "Attributes": [ + { + "Section": "4 Worker Nodes", + "SubSection": "4.1 Worker Node Configuration Files", + "Profile": "Level 1", + "AssessmentStatus": "Manual", + "Description": "If `kube-proxy` is running, ensure that the file ownership of its kubeconfig file is set to `root:root`.", + "RationaleStatement": "The kubeconfig file for `kube-proxy` controls various parameters for the `kube-proxy` service in the worker node. You should set its file ownership to maintain the integrity of the file. The file should be owned by `root:root`.", + "ImpactStatement": "None", + "RemediationProcedure": "Run the below command (based on the file location on your system) on the each worker node. For example, ``` chown root:root ```", + "AuditProcedure": "Find the kubeconfig file being used by `kube-proxy` by running the following command: ``` ps -ef | grep kube-proxy ``` If `kube-proxy` is running, get the kubeconfig file location from the `--kubeconfig` parameter. To perform the audit: Run the below command (based on the file location on your system) on the each worker node. For example, ``` stat -c %U:%G ``` Verify that the ownership is set to `root:root`.", + "AdditionalInformation": "", + "References": "https://kubernetes.io/docs/admin/kube-proxy/", + "DefaultValue": "By default, `proxy` file ownership is set to `root:root`." + } + ] + }, + { + "Id": "4.1.5", + "Description": "Ensure that the --kubeconfig kubelet.conf file permissions are set to 600 or more restrictive", + "Checks": [ + "kubelet_conf_file_permissions" + ], + "Attributes": [ + { + "Section": "4 Worker Nodes", + "SubSection": "4.1 Worker Node Configuration Files", + "Profile": "Level 1", + "AssessmentStatus": "Automated", + "Description": "Ensure that the `kubelet.conf` file has permissions of `600` or more restrictive.", + "RationaleStatement": "The `kubelet.conf` file is the kubeconfig file for the node, and controls various parameters that set the behavior and identity of the worker node. You should restrict its file permissions to maintain the integrity of the file. The file should be writable by only the administrators on the system.", + "ImpactStatement": "None", + "RemediationProcedure": "Run the below command (based on the file location on your system) on the each worker node. For example, ``` chmod 600 /etc/kubernetes/kubelet.conf ```", + "AuditProcedure": "Automated AAC auditing has been modified to allow CIS-CAT to input a variable for the / of the kubelet config file. Please set $kubelet_config= based on the file location on your system for example: ``` export kubelet_config=/etc/kubernetes/kubelet.conf ``` To perform the audit manually: Run the below command (based on the file location on your system) on the each worker node. For example, ``` stat -c %a /etc/kubernetes/kubelet.conf ``` Verify that the ownership is set to `root:root`.Verify that the permissions are `600` or more restrictive.", + "AdditionalInformation": "", + "References": "https://kubernetes.io/docs/admin/kubelet/", + "DefaultValue": "By default, `kubelet.conf` file has permissions of `600`." + } + ] + }, + { + "Id": "4.1.6", + "Description": "Ensure that the --kubeconfig kubelet.conf file ownership is set to root:root", + "Checks": [ + "kubelet_conf_file_ownership" + ], + "Attributes": [ + { + "Section": "4 Worker Nodes", + "SubSection": "4.1 Worker Node Configuration Files", + "Profile": "Level 1", + "AssessmentStatus": "Automated", + "Description": "Ensure that the `kubelet.conf` file ownership is set to `root:root`.", + "RationaleStatement": "The `kubelet.conf` file is the kubeconfig file for the node, and controls various parameters that set the behavior and identity of the worker node. You should set its file ownership to maintain the integrity of the file. The file should be owned by `root:root`.", + "ImpactStatement": "None", + "RemediationProcedure": "Run the below command (based on the file location on your system) on the each worker node. For example, ``` chown root:root /etc/kubernetes/kubelet.conf ```", + "AuditProcedure": "Automated AAC auditing has been modified to allow CIS-CAT to input a variable for the / of the kubelet config file. Please set $kubelet_config= based on the file location on your system for example: ``` export kubelet_config=/etc/kubernetes/kubelet.conf ``` To perform the audit manually: Run the below command (based on the file location on your system) on the each worker node. For example, ``` stat -c %U:%G /etc/kubernetes/kubelet.conf ``` Verify that the ownership is set to `root:root`.", + "AdditionalInformation": "", + "References": "https://kubernetes.io/docs/admin/kubelet/", + "DefaultValue": "By default, `kubelet.conf` file ownership is set to `root:root`." + } + ] + }, + { + "Id": "4.1.7", + "Description": "Ensure that the certificate authorities file permissions are set to 644 or more restrictive", + "Checks": [], + "Attributes": [ + { + "Section": "4 Worker Nodes", + "SubSection": "4.1 Worker Node Configuration Files", + "Profile": "Level 1", + "AssessmentStatus": "Manual", + "Description": "Ensure that the certificate authorities file has permissions of `644` or more restrictive.", + "RationaleStatement": "The certificate authorities file controls the authorities used to validate API requests. You should restrict its file permissions to maintain the integrity of the file. The file should be writable by only the administrators on the system.", + "ImpactStatement": "None", + "RemediationProcedure": "Run the following command to modify the file permissions of the `--client-ca-file` ``` chmod 644 ```", + "AuditProcedure": "Run the following command: ``` ps -ef | grep kubelet ``` Find the file specified by the `--client-ca-file` argument. Run the following command: ``` stat -c %a ``` Verify that the permissions are `644` or more restrictive.", + "AdditionalInformation": "", + "References": "https://kubernetes.io/docs/admin/authentication/#x509-client-certs", + "DefaultValue": "By default no `--client-ca-file` is specified." + } + ] + }, + { + "Id": "4.1.8", + "Description": "Ensure that the client certificate authorities file ownership is set to root:root", + "Checks": [], + "Attributes": [ + { + "Section": "4 Worker Nodes", + "SubSection": "4.1 Worker Node Configuration Files", + "Profile": "Level 1", + "AssessmentStatus": "Manual", + "Description": "Ensure that the certificate authorities file ownership is set to `root:root`.", + "RationaleStatement": "The certificate authorities file controls the authorities used to validate API requests. You should set its file ownership to maintain the integrity of the file. The file should be owned by `root:root`.", + "ImpactStatement": "None", + "RemediationProcedure": "Run the following command to modify the ownership of the `--client-ca-file`. ``` chown root:root ```", + "AuditProcedure": "Run the following command: ``` ps -ef | grep kubelet ``` Find the file specified by the `--client-ca-file` argument. Run the following command: ``` stat -c %U:%G ``` Verify that the ownership is set to `root:root`.", + "AdditionalInformation": "", + "References": "https://kubernetes.io/docs/admin/authentication/#x509-client-certs", + "DefaultValue": "By default no `--client-ca-file` is specified." + } + ] + }, + { + "Id": "4.1.9", + "Description": "If the kubelet config.yaml configuration file is being used validate permissions set to 600 or more restrictive", + "Checks": [ + "kubelet_config_yaml_permissions" + ], + "Attributes": [ + { + "Section": "4 Worker Nodes", + "SubSection": "4.1 Worker Node Configuration Files", + "Profile": "Level 1", + "AssessmentStatus": "Automated", + "Description": "Ensure that if the kubelet refers to a configuration file with the `--config` argument, that file has permissions of 600 or more restrictive.", + "RationaleStatement": "The kubelet reads various parameters, including security settings, from a config file specified by the `--config` argument. If this file is specified you should restrict its file permissions to maintain the integrity of the file. The file should be writable by only the administrators on the system.", + "ImpactStatement": "None", + "RemediationProcedure": "Run the following command (using the config file location identified in the Audit step) ``` chmod 600 /var/lib/kubelet/config.yaml ```", + "AuditProcedure": "Automated AAC auditing has been modified to allow CIS-CAT to input a variable for the / of the kubelet config yaml file. Please set $kubelet_config_yaml= based on the file location on your system for example: ``` export kubelet_config_yaml=/var/lib/kubelet/config.yaml ``` To perform the audit manually: Run the below command (based on the file location on your system) on the each worker node. For example, ``` stat -c %a /var/lib/kubelet/config.yaml ``` Verify that the permissions are `600` or more restrictive.", + "AdditionalInformation": "", + "References": "https://kubernetes.io/docs/tasks/administer-cluster/kubelet-config-file/", + "DefaultValue": "By default, the /var/lib/kubelet/config.yaml file as set up by `kubeadm` has permissions of 600." + } + ] + }, + { + "Id": "4.1.10", + "Description": "If the kubelet config.yaml configuration file is being used validate file ownership is set to root:root", + "Checks": [ + "kubelet_config_yaml_ownership" + ], + "Attributes": [ + { + "Section": "4 Worker Nodes", + "SubSection": "4.1 Worker Node Configuration Files", + "Profile": "Level 1", + "AssessmentStatus": "Automated", + "Description": "Ensure that if the kubelet refers to a configuration file with the `--config` argument, that file is owned by root:root.", + "RationaleStatement": "The kubelet reads various parameters, including security settings, from a config file specified by the `--config` argument. If this file is specified you should restrict its file permissions to maintain the integrity of the file. The file should be owned by root:root.", + "ImpactStatement": "None", + "RemediationProcedure": "Run the following command (using the config file location identied in the Audit step) ``` chown root:root /etc/kubernetes/kubelet.conf ```", + "AuditProcedure": "Automated AAC auditing has been modified to allow CIS-CAT to input a variable for the / of the kubelet config yaml file. Please set $kubelet_config_yaml= based on the file location on your system for example: ``` export kubelet_config_yaml=/var/lib/kubelet/config.yaml ``` To perform the audit manually: Run the below command (based on the file location on your system) on the each worker node. For example, ``` stat -c %U:%G /var/lib/kubelet/config.yaml ```Verify that the ownership is set to `root:root`.", + "AdditionalInformation": "", + "References": "https://kubernetes.io/docs/tasks/administer-cluster/kubelet-config-file/", + "DefaultValue": "By default, `/var/lib/kubelet/config.yaml` file as set up by `kubeadm` is owned by `root:root`." + } + ] + }, + { + "Id": "4.2.1", + "Description": "Ensure that the --anonymous-auth argument is set to false", + "Checks": [ + "kubelet_disable_anonymous_auth" + ], + "Attributes": [ + { + "Section": "4 Worker Nodes", + "SubSection": "4.2 Kubelet", + "Profile": "Level 1", + "AssessmentStatus": "Automated", + "Description": "Disable anonymous requests to the Kubelet server.", + "RationaleStatement": "When enabled, requests that are not rejected by other configured authentication methods are treated as anonymous requests. These requests are then served by the Kubelet server. You should rely on authentication to authorize access and disallow anonymous requests.", + "ImpactStatement": "Anonymous requests will be rejected.", + "RemediationProcedure": "If using a Kubelet config file, edit the file to set `authentication: anonymous: enabled` to `false`. If using executable arguments, edit the kubelet service file `/etc/kubernetes/kubelet.conf` on each worker node and set the below parameter in `KUBELET_SYSTEM_PODS_ARGS` variable. ``` --anonymous-auth=false ``` Based on your system, restart the `kubelet` service. For example: ``` systemctl daemon-reload systemctl restart kubelet.service ```", + "AuditProcedure": "If using a Kubelet configuration file, check that there is an entry for `authentication: anonymous: enabled` set to `false`. Run the following command on each node: ``` ps -ef | grep kubelet ``` Verify that the `--anonymous-auth` argument is set to `false`. This executable argument may be omitted, provided there is a corresponding entry set to `false` in the Kubelet config file.", + "AdditionalInformation": "", + "References": "https://kubernetes.io/docs/admin/kubelet/:https://kubernetes.io/docs/admin/kubelet-authentication-authorization/#kubelet-authentication", + "DefaultValue": "By default, anonymous access is enabled." + } + ] + }, + { + "Id": "4.2.2", + "Description": "Ensure that the --authorization-mode argument is not set to AlwaysAllow", + "Checks": [ + "kubelet_authorization_mode" + ], + "Attributes": [ + { + "Section": "4 Worker Nodes", + "SubSection": "4.2 Kubelet", + "Profile": "Level 1", + "AssessmentStatus": "Automated", + "Description": "Do not allow all requests. Enable explicit authorization.", + "RationaleStatement": "Kubelets, by default, allow all authenticated requests (even anonymous ones) without needing explicit authorization checks from the apiserver. You should restrict this behavior and only allow explicitly authorized requests.", + "ImpactStatement": "Unauthorized requests will be denied.", + "RemediationProcedure": "If using a Kubelet config file, edit the file to set `authorization: mode` to `Webhook`. If using executable arguments, edit the kubelet service file `/etc/kubernetes/kubelet.conf` on each worker node and set the below parameter in `KUBELET_AUTHZ_ARGS` variable. ``` --authorization-mode=Webhook ``` Based on your system, restart the `kubelet` service. For example: ``` systemctl daemon-reload systemctl restart kubelet.service ```", + "AuditProcedure": "Run the following command on each node: ``` ps -ef | grep kubelet ``` If the `--authorization-mode` argument is present check that it is not set to `AlwaysAllow`. If it is not present check that there is a Kubelet config file specified by `--config`, and that file sets `authorization: mode` to something other than `AlwaysAllow`. It is also possible to review the running configuration of a Kubelet via the `/configz` endpoint on the Kubelet API port (typically `10250/TCP`). Accessing these with appropriate credentials will provide details of the Kubelet's configuration.", + "AdditionalInformation": "", + "References": "https://kubernetes.io/docs/admin/kubelet/:https://kubernetes.io/docs/admin/kubelet-authentication-authorization/#kubelet-authentication", + "DefaultValue": "By default, `--authorization-mode` argument is set to `AlwaysAllow`." + } + ] + }, + { + "Id": "4.2.3", + "Description": "Ensure that the --client-ca-file argument is set as appropriate", + "Checks": [ + "kubelet_client_ca_file_set" + ], + "Attributes": [ + { + "Section": "4 Worker Nodes", + "SubSection": "4.2 Kubelet", + "Profile": "Level 1", + "AssessmentStatus": "Automated", + "Description": "Enable Kubelet authentication using certificates.", + "RationaleStatement": "The connections from the apiserver to the kubelet are used for fetching logs for pods, attaching (through kubectl) to running pods, and using the kubelet’s port-forwarding functionality. These connections terminate at the kubelet’s HTTPS endpoint. By default, the apiserver does not verify the kubelet’s serving certificate, which makes the connection subject to man-in-the-middle attacks, and unsafe to run over untrusted and/or public networks. Enabling Kubelet certificate authentication ensures that the apiserver could authenticate the Kubelet before submitting any requests.", + "ImpactStatement": "You require TLS to be configured on apiserver as well as kubelets.", + "RemediationProcedure": "If using a Kubelet config file, edit the file to set `authentication: x509: clientCAFile` to the location of the client CA file. If using command line arguments, edit the kubelet service file `/etc/kubernetes/kubelet.conf` on each worker node and set the below parameter in `KUBELET_AUTHZ_ARGS` variable. ``` --client-ca-file= ``` Based on your system, restart the `kubelet` service. For example: ``` systemctl daemon-reload systemctl restart kubelet.service ```", + "AuditProcedure": "Run the following command on each node: ``` ps -ef | grep kubelet ``` Verify that the `--client-ca-file` argument exists and is set to the location of the client certificate authority file. If the `--client-ca-file` argument is not present, check that there is a Kubelet config file specified by `--config`, and that the file sets `authentication: x509: clientCAFile` to the location of the client certificate authority file.", + "AdditionalInformation": "", + "References": "https://kubernetes.io/docs/admin/kubelet/:https://kubernetes.io/docs/reference/command-line-tools-reference/kubelet-authentication-authorization/", + "DefaultValue": "By default, `--client-ca-file` argument is not set." + } + ] + }, + { + "Id": "4.2.4", + "Description": "Verify that if defined, readOnlyPort is set to 0", + "Checks": [ + "kubelet_disable_read_only_port" + ], + "Attributes": [ + { + "Section": "4 Worker Nodes", + "SubSection": "4.2 Kubelet", + "Profile": "Level 1", + "AssessmentStatus": "Manual", + "Description": "Disable the read-only port.", + "RationaleStatement": "The Kubelet process provides a read-only API in addition to the main Kubelet API. Unauthenticated access is provided to this read-only API which could possibly retrieve potentially sensitive information about the cluster.", + "ImpactStatement": "Removal of the read-only port will require that any service which made use of it will need to be re-configured to use the main Kubelet API.", + "RemediationProcedure": "If using a Kubelet config file, edit the file to set `readOnlyPort` to `0`. If using command line arguments, edit the kubelet service file `/etc/kubernetes/kubelet.conf` on each worker node and set the below parameter in `KUBELET_SYSTEM_PODS_ARGS` variable. ``` --read-only-port=0 ``` Based on your system, restart the `kubelet` service. For example: ``` systemctl daemon-reload systemctl restart kubelet.service ```", + "AuditProcedure": "Run the following command on each node: ``` ps -ef | grep kubelet ``` Verify that the `--read-only-port` argument exists and is set to `0`. If the `--read-only-port` argument is not present, check that there is a Kubelet config file specified by `--config`. Check that if there is a `readOnlyPort` entry in the file, it is set to `0`.", + "AdditionalInformation": "https://kubernetes.io/docs/reference/command-line-tools-reference/kubelet/", + "References": "https://kubernetes.io/docs/admin/kubelet/:https://github.com/kubernetes/kubernetes/blob/6cedc0853faa118df0ba3d41b48b993422ad3df6/staging/src/k8s.io/kubelet/config/v1beta1/types.go#L142", + "DefaultValue": "" + } + ] + }, + { + "Id": "4.2.5", + "Description": "Ensure that the --streaming-connection-idle-timeout argument is not set to 0", + "Checks": [ + "kubelet_streaming_connection_timeout" + ], + "Attributes": [ + { + "Section": "4 Worker Nodes", + "SubSection": "4.2 Kubelet", + "Profile": "Level 1", + "AssessmentStatus": "Manual", + "Description": "Do not disable timeouts on streaming connections.", + "RationaleStatement": "Setting idle timeouts ensures that you are protected against Denial-of-Service attacks, inactive connections and running out of ephemeral ports. **Note:** By default, `--streaming-connection-idle-timeout` is set to 4 hours which might be too high for your environment. Setting this as appropriate would additionally ensure that such streaming connections are timed out after serving legitimate use cases.", + "ImpactStatement": "Long-lived connections could be interrupted.", + "RemediationProcedure": "If using a Kubelet config file, edit the file to set `streamingConnectionIdleTimeout` to a value other than 0. If using command line arguments, edit the kubelet service file `/etc/kubernetes/kubelet.conf` on each worker node and set the below parameter in `KUBELET_SYSTEM_PODS_ARGS` variable. ``` --streaming-connection-idle-timeout=5m ``` Based on your system, restart the `kubelet` service. For example: ``` systemctl daemon-reload systemctl restart kubelet.service ```", + "AuditProcedure": "Run the following command on each node: ``` ps -ef | grep kubelet ``` Verify that the `--streaming-connection-idle-timeout` argument is not set to `0`. If the argument is not present, and there is a Kubelet config file specified by `--config`, check that it does not set `streamingConnectionIdleTimeout` to 0.", + "AdditionalInformation": "", + "References": "https://kubernetes.io/docs/admin/kubelet/:https://github.com/kubernetes/kubernetes/pull/18552", + "DefaultValue": "By default, `--streaming-connection-idle-timeout` is set to 4 hours." + } + ] + }, + { + "Id": "4.2.6", + "Description": "Ensure that the --make-iptables-util-chains argument is set to true", + "Checks": [ + "kubelet_manage_iptables" + ], + "Attributes": [ + { + "Section": "4 Worker Nodes", + "SubSection": "4.2 Kubelet", + "Profile": "Level 1", + "AssessmentStatus": "Automated", + "Description": "Allow Kubelet to manage iptables.", + "RationaleStatement": "Kubelets can automatically manage the required changes to iptables based on how you choose your networking options for the pods. It is recommended to let kubelets manage the changes to iptables. This ensures that the iptables configuration remains in sync with pods networking configuration. Manually configuring iptables with dynamic pod network configuration changes might hamper the communication between pods/containers and to the outside world. You might have iptables rules too restrictive or too open.", + "ImpactStatement": "Kubelet would manage the iptables on the system and keep it in sync. If you are using any other iptables management solution, then there might be some conflicts.", + "RemediationProcedure": "If using a Kubelet config file, edit the file to set `makeIPTablesUtilChains: true`. If using command line arguments, edit the kubelet service file `/etc/kubernetes/kubelet.conf` on each worker node and remove the `--make-iptables-util-chains` argument from the `KUBELET_SYSTEM_PODS_ARGS` variable. Based on your system, restart the `kubelet` service. For example: ``` systemctl daemon-reload systemctl restart kubelet.service ```", + "AuditProcedure": "Run the following command on each node: ``` ps -ef | grep kubelet ``` Verify that if the `--make-iptables-util-chains` argument exists then it is set to `true`. If the `--make-iptables-util-chains` argument does not exist, and there is a Kubelet config file specified by `--config`, verify that the file does not set `makeIPTablesUtilChains` to `false`.", + "AdditionalInformation": "", + "References": "https://kubernetes.io/docs/admin/kubelet/", + "DefaultValue": "By default, `--make-iptables-util-chains` argument is set to `true`." + } + ] + }, + { + "Id": "4.2.7", + "Description": "Ensure that the --hostname-override argument is not set", + "Checks": [], + "Attributes": [ + { + "Section": "4 Worker Nodes", + "SubSection": "4.2 Kubelet", + "Profile": "Level 1", + "AssessmentStatus": "Manual", + "Description": "Do not override node hostnames.", + "RationaleStatement": "Overriding hostnames could potentially break TLS setup between the kubelet and the apiserver. Additionally, with overridden hostnames, it becomes increasingly difficult to associate logs with a particular node and process them for security analytics. Hence, you should setup your kubelet nodes with resolvable FQDNs and avoid overriding the hostnames with IPs.", + "ImpactStatement": "Some cloud providers may require this flag to ensure that hostname matches names issued by the cloud provider. In these environments, this recommendation should not apply.", + "RemediationProcedure": "Edit the kubelet service file `/etc/systemd/system/kubelet.service.d/10-kubeadm.conf` on each worker node and remove the `--hostname-override` argument from the `KUBELET_SYSTEM_PODS_ARGS` variable. Based on your system, restart the `kubelet` service. For example: ``` systemctl daemon-reload systemctl restart kubelet.service ```", + "AuditProcedure": "Run the following command on each node: ``` ps -ef | grep kubelet ``` Verify that `--hostname-override` argument does not exist. **Note** This setting is not configurable via the Kubelet config file.", + "AdditionalInformation": "", + "References": "https://kubernetes.io/docs/admin/kubelet/:https://github.com/kubernetes/kubernetes/issues/22063", + "DefaultValue": "By default, `--hostname-override` argument is not set." + } + ] + }, + { + "Id": "4.2.8", + "Description": "Ensure that the eventRecordQPS argument is set to a level which ensures appropriate event capture", + "Checks": [ + "kubelet_event_record_qps" + ], + "Attributes": [ + { + "Section": "4 Worker Nodes", + "SubSection": "4.2 Kubelet", + "Profile": "Level 2", + "AssessmentStatus": "Manual", + "Description": "Security relevant information should be captured. The eventRecordQPS on the Kubelet configuration can be used to limit the rate at which events are gathered and sets the maximum event creations per second. Setting this too low could result in relevant events not being logged, however the unlimited setting of `0` could result in a denial of service on the kubelet.", + "RationaleStatement": "It is important to capture all events and not restrict event creation. Events are an important source of security information and analytics that ensure that your environment is consistently monitored using the event data.", + "ImpactStatement": "Setting this parameter to `0` could result in a denial of service condition due to excessive events being created. The cluster's event processing and storage systems should be scaled to handle expected event loads.", + "RemediationProcedure": "If using a Kubelet config file, edit the file to set `eventRecordQPS:` to an appropriate level. If using command line arguments, edit the kubelet service file `/etc/systemd/system/kubelet.service.d/10-kubeadm.conf` on each worker node and set the below parameter in `KUBELET_ARGS` variable. Based on your system, restart the `kubelet` service. For example: ``` systemctl daemon-reload systemctl restart kubelet.service ```", + "AuditProcedure": "Run the following command on each node: ``` sudo grep eventRecordQPS /etc/systemd/system/kubelet.service.d/10-kubeadm.conf ``` or If using command line arguments, kubelet service file is located /etc/systemd/system/kubelet.service.d/10-kubelet-args.conf ``` sudo grep eventRecordQPS /etc/systemd/system/kubelet.service.d/10-kubelet-args.conf ``` Review the value set for the argument and determine whether this has been set to an appropriate level for the cluster. If the argument does not exist, check that there is a Kubelet config file specified by `--config` and review the value in this location. If using command line arguments", + "AdditionalInformation": "", + "References": "https://kubernetes.io/docs/admin/kubelet/:https://github.com/kubernetes/kubernetes/blob/master/pkg/kubelet/apis/kubeletconfig/v1beta1/types.go", + "DefaultValue": "By default, eventRecordQPS argument is set to `5`." + } + ] + }, + { + "Id": "4.2.9", + "Description": "Ensure that the --tls-cert-file and --tls-private-key-file arguments are set as appropriate", + "Checks": [ + "kubelet_tls_cert_and_key" + ], + "Attributes": [ + { + "Section": "4 Worker Nodes", + "SubSection": "4.2 Kubelet", + "Profile": "Level 1", + "AssessmentStatus": "Manual", + "Description": "Setup TLS connection on the Kubelets.", + "RationaleStatement": "The connections from the apiserver to the kubelet are used for fetching logs for pods, attaching (through kubectl) to running pods, and using the kubelet’s port-forwarding functionality. These connections terminate at the kubelet’s HTTPS endpoint. By default, the apiserver does not verify the kubelet’s serving certificate, which makes the connection subject to man-in-the-middle attacks, and unsafe to run over untrusted and/or public networks.", + "ImpactStatement": "", + "RemediationProcedure": "If using a Kubelet config file, edit the file to set tlsCertFile to the location of the certificate file to use to identify this Kubelet, and tlsPrivateKeyFile to the location of the corresponding private key file. If using command line arguments, edit the kubelet service file /etc/kubernetes/kubelet.conf on each worker node and set the below parameters in KUBELET_CERTIFICATE_ARGS variable. --tls-cert-file= --tls-private-key-file= Based on your system, restart the kubelet service. For example: ``` systemctl daemon-reload systemctl restart kubelet.service ```", + "AuditProcedure": "Run the following command on each node: ``` ps -ef | grep kubelet ``` Verify that the --tls-cert-file and --tls-private-key-file arguments exist and they are set as appropriate. If these arguments are not present, check that there is a Kubelet config specified by --config and that it contains appropriate settings for tlsCertFile and tlsPrivateKeyFile.", + "AdditionalInformation": "", + "References": "", + "DefaultValue": "" + } + ] + }, + { + "Id": "4.2.10", + "Description": "Ensure that the --rotate-certificates argument is not set to false", + "Checks": [ + "kubelet_rotate_certificates" + ], + "Attributes": [ + { + "Section": "4 Worker Nodes", + "SubSection": "4.2 Kubelet", + "Profile": "Level 1", + "AssessmentStatus": "Automated", + "Description": "Enable kubelet client certificate rotation.", + "RationaleStatement": "The `--rotate-certificates` setting causes the kubelet to rotate its client certificates by creating new CSRs as its existing credentials expire. This automated periodic rotation ensures that the there is no downtime due to expired certificates and thus addressing availability in the CIA security triad. **Note:** This recommendation only applies if you let kubelets get their certificates from the API server. In case your kubelet certificates come from an outside authority/tool (e.g. Vault) then you need to take care of rotation yourself. **Note:** This feature also require the `RotateKubeletClientCertificate` feature gate to be enabled (which is the default since Kubernetes v1.7)", + "ImpactStatement": "None", + "RemediationProcedure": "If using a Kubelet config file, edit the file to add the line `rotateCertificates: true` or remove it altogether to use the default value. If using command line arguments, edit the kubelet service file `/etc/kubernetes/kubelet.conf` on each worker node and remove `--rotate-certificates=false` argument from the `KUBELET_CERTIFICATE_ARGS` variable or set --rotate-certificates=true . Based on your system, restart the `kubelet` service. For example: ``` systemctl daemon-reload systemctl restart kubelet.service ```", + "AuditProcedure": "Run the following command on each node: ``` ps -ef | grep kubelet ``` Verify that the `RotateKubeletServerCertificate` argument is not present, or is set to `true`. If the RotateKubeletServerCertificate argument is not present, verify that if there is a Kubelet config file specified by `--config`, that file does not contain `RotateKubeletServerCertificate: false`.", + "AdditionalInformation": "", + "References": "https://github.com/kubernetes/kubernetes/pull/41912:https://kubernetes.io/docs/reference/command-line-tools-reference/kubelet-tls-bootstrapping/#kubelet-configuration:https://kubernetes.io/docs/imported/release/notes/:https://kubernetes.io/docs/reference/command-line-tools-reference/feature-gates/", + "DefaultValue": "By default, kubelet client certificate rotation is enabled." + } + ] + }, + { + "Id": "4.2.11", + "Description": "Verify that the RotateKubeletServerCertificate argument is set to true", + "Checks": [], + "Attributes": [ + { + "Section": "4 Worker Nodes", + "SubSection": "4.2 Kubelet", + "Profile": "Level 1", + "AssessmentStatus": "Manual", + "Description": "Enable kubelet server certificate rotation.", + "RationaleStatement": "`RotateKubeletServerCertificate` causes the kubelet to both request a serving certificate after bootstrapping its client credentials and rotate the certificate as its existing credentials expire. This automated periodic rotation ensures that the there are no downtimes due to expired certificates and thus addressing availability in the CIA security triad. Note: This recommendation only applies if you let kubelets get their certificates from the API server. In case your kubelet certificates come from an outside authority/tool (e.g. Vault) then you need to take care of rotation yourself.", + "ImpactStatement": "None", + "RemediationProcedure": "Edit the kubelet service file `/etc/kubernetes/kubelet.conf` on each worker node and set the below parameter in `KUBELET_CERTIFICATE_ARGS` variable. ``` --feature-gates=RotateKubeletServerCertificate=true ``` Based on your system, restart the `kubelet` service. For example: ``` systemctl daemon-reload systemctl restart kubelet.service ```", + "AuditProcedure": "Ignore this check if serverTLSBootstrap is true in the kubelet config file or if the --rotate-server-certificates parameter is set on kubelet Run the following command on each node: ``` ps -ef | grep kubelet ``` Verify that `RotateKubeletServerCertificate` argument exists and is set to `true`.", + "AdditionalInformation": "", + "References": "https://github.com/kubernetes/kubernetes/pull/45059:https://kubernetes.io/docs/admin/kubelet-tls-bootstrapping/#kubelet-configuration", + "DefaultValue": "By default, kubelet server certificate rotation is enabled." + } + ] + }, + { + "Id": "4.2.12", + "Description": "Ensure that the Kubelet only makes use of Strong Cryptographic Ciphers", + "Checks": [ + "kubelet_strong_ciphers_only" + ], + "Attributes": [ + { + "Section": "4 Worker Nodes", + "SubSection": "4.2 Kubelet", + "Profile": "Level 1", + "AssessmentStatus": "Manual", + "Description": "Ensure that the Kubelet is configured to only use strong cryptographic ciphers.", + "RationaleStatement": "TLS ciphers have had a number of known vulnerabilities and weaknesses, which can reduce the protection provided by them. By default Kubernetes supports a number of TLS ciphersuites including some that have security concerns, weakening the protection provided.", + "ImpactStatement": "Kubelet clients that cannot support modern cryptographic ciphers will not be able to make connections to the Kubelet API.", + "RemediationProcedure": "If using a Kubelet config file, edit the file to set `tlsCipherSuites:` to `TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305,TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305,TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,TLS_RSA_WITH_AES_256_GCM_SHA384,TLS_RSA_WITH_AES_128_GCM_SHA256` or to a subset of these values. If using executable arguments, edit the kubelet service file `/etc/kubernetes/kubelet.conf` on each worker node and set the `--tls-cipher-suites` parameter as follows, or to a subset of these values. ``` --tls-cipher-suites=TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305,TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305,TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,TLS_RSA_WITH_AES_256_GCM_SHA384,TLS_RSA_WITH_AES_128_GCM_SHA256 ``` Based on your system, restart the `kubelet` service. For example: ``` systemctl daemon-reload systemctl restart kubelet.service ```", + "AuditProcedure": "The set of cryptographic ciphers currently considered secure is the following: - `TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256` - `TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256` - `TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305` - `TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384` - `TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305` - `TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384` - `TLS_RSA_WITH_AES_256_GCM_SHA384` - `TLS_RSA_WITH_AES_128_GCM_SHA256` Run the following command on each node: ``` ps -ef | grep kubelet ``` If the `--tls-cipher-suites` argument is present, ensure it only contains values included in this set. If it is not present check that there is a Kubelet config file specified by `--config`, and that file sets `tlsCipherSuites:` to only include values from this set.", + "AdditionalInformation": "The list chosen above should be fine for modern clients. It's essentially the list from the Mozilla Modern cipher option with the ciphersuites supporting CBC mode removed, as CBC has traditionally had a lot of issues", + "References": "", + "DefaultValue": "By default the Kubernetes API server supports a wide range of TLS ciphers" + } + ] + }, + { + "Id": "4.2.13", + "Description": "Ensure that a limit is set on pod PIDs", + "Checks": [], + "Attributes": [ + { + "Section": "4 Worker Nodes", + "SubSection": "4.2 Kubelet", + "Profile": "Level 1", + "AssessmentStatus": "Manual", + "Description": "Ensure that the Kubelet sets limits on the number of PIDs that can be created by pods running on the node.", + "RationaleStatement": "By default pods running in a cluster can consume any number of PIDs, potentially exhausting the resources available on the node. Setting an appropriate limit reduces the risk of a denial of service attack on cluster nodes.", + "ImpactStatement": "Setting this value will restrict the number of processes per pod. If this limit is lower than the number of PIDs required by a pod it will not operate.", + "RemediationProcedure": "Decide on an appropriate level for this parameter and set it, either via the `--pod-max-pids` command line parameter or the `PodPidsLimit` configuration file setting.", + "AuditProcedure": "Review the Kubelet's start-up parameters for the value of `--pod-max-pids`, and check the Kubelet configuration file for the `PodPidsLimit` . If neither of these values is set, then there is no limit in place.", + "AdditionalInformation": "", + "References": "https://kubernetes.io/docs/concepts/policy/pid-limiting/#pod-pid-limits", + "DefaultValue": "By default the number of PIDs is not limited." + } + ] + }, + { + "Id": "4.2.14", + "Description": "Ensure that the --seccomp-default parameter is set to true", + "Checks": [], + "Attributes": [ + { + "Section": "4 Worker Nodes", + "SubSection": "4.2 Kubelet", + "Profile": "Level 1", + "AssessmentStatus": "Manual", + "Description": "Ensure that the Kubelet enforces the use of the RuntimeDefault seccomp profile", + "RationaleStatement": "By default, Kubernetes disables the seccomp profile which ships with most container runtimes. Setting this parameter will ensure workloads running on the node are protected by the runtime's seccomp profile.", + "ImpactStatement": "Setting this will remove some rights from pods running on the node.", + "RemediationProcedure": "Set the parameter, either via the `--seccomp-default` command line parameter or the `seccompDefault` configuration file setting.", + "AuditProcedure": "Review the Kubelet's start-up parameters for the value of `--seccomp-default`, and check the Kubelet configuration file for the `seccompDefault` . If neither of these values is set, then the seccomp profile is not in use.", + "AdditionalInformation": "", + "References": "https://kubernetes.io/docs/tutorials/security/seccomp/#enable-the-use-of-runtimedefault-as-the-default-seccomp-profile-for-all-workloads", + "DefaultValue": "By default the seccomp profile is not enabled." + } + ] + }, + { + "Id": "4.3.1", + "Description": "Ensure that the kube-proxy metrics service is bound to localhost", + "Checks": [], + "Attributes": [ + { + "Section": "4 Worker Nodes", + "SubSection": "4.3 kube-proxy", + "Profile": "Level 1", + "AssessmentStatus": "Manual", + "Description": "Do not bind the kube-proxy metrics port to non-loopback addresses.", + "RationaleStatement": "kube-proxy has two APIs which provided access to information about the service and can be bound to network ports. The metrics API service includes endpoints (`/metrics` and `/configz`) which disclose information about the configuration and operation of kube-proxy. These endpoints should not be exposed to untrusted networks as they do not support encryption or authentication to restrict access to the data they provide.", + "ImpactStatement": "3rd party services which try to access metrics or configuration information related to kube-proxy will require access to the localhost interface of the node.", + "RemediationProcedure": "Modify or remove any values which bind the metrics service to a non-localhost address", + "AuditProcedure": "review the start-up flags provided to kube proxy Run the following command on each node: ``` ps -ef | grep -i kube-proxy ``` Ensure that the `--metrics-bind-address` parameter is not set to a value other than 127.0.0.1. From the output of this command gather the location specified in the `--config` parameter. Review any file stored at that location and ensure that it does not specify a value other than 127.0.0.1 for `metricsBindAddress`.", + "AdditionalInformation": "", + "References": "https://kubernetes.io/docs/reference/command-line-tools-reference/kube-proxy/", + "DefaultValue": "The default value is `127.0.0.1:10249`" + } + ] + }, + { + "Id": "5.1.1", + "Description": "Ensure that the cluster-admin role is only used where required", + "Checks": [ + "rbac_cluster_admin_usage" + ], + "Attributes": [ + { + "Section": "5 Policies", + "SubSection": "5.1 RBAC and Service Accounts", + "Profile": "Level 1", + "AssessmentStatus": "Manual", + "Description": "The RBAC role `cluster-admin` provides wide-ranging powers over the environment and should be used only where and when needed.", + "RationaleStatement": "Kubernetes provides a set of default roles where RBAC is used. Some of these roles such as `cluster-admin` provide wide-ranging privileges which should only be applied where absolutely necessary. Roles such as `cluster-admin` allow super-user access to perform any action on any resource. When used in a `ClusterRoleBinding`, it gives full control over every resource in the cluster and in all namespaces. When used in a `RoleBinding`, it gives full control over every resource in the rolebinding's namespace, including the namespace itself.", + "ImpactStatement": "Care should be taken before removing any `clusterrolebindings` from the environment to ensure they were not required for operation of the cluster. Specifically, modifications should not be made to `clusterrolebindings` with the `system:` prefix as they are required for the operation of system components.", + "RemediationProcedure": "Identify all clusterrolebindings to the cluster-admin role. Check if they are used and if they need this role or if they could use a role with fewer privileges. Where possible, first bind users to a lower privileged role and then remove the clusterrolebinding to the cluster-admin role : ``` kubectl delete clusterrolebinding [name] ```", + "AuditProcedure": "Obtain a list of the principals who have access to the `cluster-admin` role by reviewing the `clusterrolebinding` output for each role binding that has access to the `cluster-admin` role. ``` kubectl get clusterrolebindings -o=custom-columns=NAME:.metadata.name,ROLE:.roleRef.name,SUBJECT:.subjects[*].name ``` Review each principal listed and ensure that `cluster-admin` privilege is required for it.", + "AdditionalInformation": "", + "References": "https://kubernetes.io/docs/admin/authorization/rbac/#user-facing-roles", + "DefaultValue": "By default a single `clusterrolebinding` called `cluster-admin` is provided with the `system:masters` group as its principal." + } + ] + }, + { + "Id": "5.1.2", + "Description": "Minimize access to secrets", + "Checks": [ + "rbac_minimize_secret_access" + ], + "Attributes": [ + { + "Section": "5 Policies", + "SubSection": "5.1 RBAC and Service Accounts", + "Profile": "Level 1", + "AssessmentStatus": "Manual", + "Description": "The Kubernetes API stores secrets, which may be service account tokens for the Kubernetes API or credentials used by workloads in the cluster. Access to these secrets should be restricted to the smallest possible group of users to reduce the risk of privilege escalation.", + "RationaleStatement": "Inappropriate access to secrets stored within the Kubernetes cluster can allow for an attacker to gain additional access to the Kubernetes cluster or external resources whose credentials are stored as secrets.", + "ImpactStatement": "Care should be taken not to remove access to secrets to system components which require this for their operation", + "RemediationProcedure": "Where possible, restrict access to secret objects in the cluster by removing get, list, and watch permissions.", + "AuditProcedure": "Review the users who have `get`, `list`, or `watch` access to `secrets` objects in the Kubernetes API.", + "AdditionalInformation": "", + "References": "", + "DefaultValue": "By default in a kubeadm cluster the following list of principals have `get` privileges on `secret` objects ``` CLUSTERROLEBINDING SUBJECT TYPE SA-NAMESPACE cluster-admin system:masters Group system:controller:clusterrole-aggregation-controller clusterrole-aggregation-controller ServiceAccount kube-system system:controller:expand-controller expand-controller ServiceAccount kube-system system:controller:generic-garbage-collector generic-garbage-collector ServiceAccount kube-system system:controller:namespace-controller namespace-controller ServiceAccount kube-system system:controller:persistent-volume-binder persistent-volume-binder ServiceAccount kube-system system:kube-controller-manager system:kube-controller-manager User ```" + } + ] + }, + { + "Id": "5.1.3", + "Description": "Minimize wildcard use in Roles and ClusterRoles", + "Checks": [ + "rbac_minimize_wildcard_use_roles" + ], + "Attributes": [ + { + "Section": "5 Policies", + "SubSection": "5.1 RBAC and Service Accounts", + "Profile": "Level 1", + "AssessmentStatus": "Manual", + "Description": "Kubernetes Roles and ClusterRoles provide access to resources based on sets of objects and actions that can be taken on those objects. It is possible to set either of these to be the wildcard * which matches all items. Use of wildcards is not optimal from a security perspective as it may allow for inadvertent access to be granted when new resources are added to the Kubernetes API either as CRDs or in later versions of the product.", + "RationaleStatement": "The principle of least privilege recommends that users are provided only the access required for their role and nothing more. The use of wildcard rights grants is likely to provide excessive rights to the Kubernetes API.", + "ImpactStatement": "", + "RemediationProcedure": "Where possible replace any use of wildcards in ClusterRoles and Roles with specific objects or actions.", + "AuditProcedure": "Retrieve the roles defined across each namespaces in the cluster and review for wildcards ``` kubectl get roles --all-namespaces -o yaml ``` Retrieve the cluster roles defined in the cluster and review for wildcards ``` kubectl get clusterroles -o yaml ```", + "AdditionalInformation": "", + "References": "", + "DefaultValue": "" + } + ] + }, + { + "Id": "5.1.4", + "Description": "Minimize access to create pods", + "Checks": [ + "rbac_minimize_pod_creation_access" + ], + "Attributes": [ + { + "Section": "5 Policies", + "SubSection": "5.1 RBAC and Service Accounts", + "Profile": "Level 1", + "AssessmentStatus": "Manual", + "Description": "The ability to create pods in a namespace can provide a number of opportunities for privilege escalation, such as assigning privileged service accounts to these pods or mounting hostPaths with access to sensitive data (unless Pod Security Policies are implemented to restrict this access) As such, access to create new pods should be restricted to the smallest possible group of users.", + "RationaleStatement": "The ability to create pods in a cluster opens up possibilities for privilege escalation and should be restricted, where possible.", + "ImpactStatement": "Care should be taken not to remove access to pods to system components which require this for their operation", + "RemediationProcedure": "Where possible, remove `create` access to `pod` objects in the cluster.", + "AuditProcedure": "Review the users who have create access to pod objects in the Kubernetes API.", + "AdditionalInformation": "", + "References": "", + "DefaultValue": "By default in a kubeadm cluster the following list of principals have `create` privileges on `pod` objects ``` CLUSTERROLEBINDING SUBJECT TYPE SA-NAMESPACE cluster-admin system:masters Group system:controller:clusterrole-aggregation-controller clusterrole-aggregation-controller ServiceAccount kube-system system:controller:daemon-set-controller daemon-set-controller ServiceAccount kube-system system:controller:job-controller job-controller ServiceAccount kube-system system:controller:persistent-volume-binder persistent-volume-binder ServiceAccount kube-system system:controller:replicaset-controller replicaset-controller ServiceAccount kube-system system:controller:replication-controller replication-controller ServiceAccount kube-system system:controller:statefulset-controller statefulset-controller ServiceAccount kube-system ```" + } + ] + }, + { + "Id": "5.1.5", + "Description": "Ensure that default service accounts are not actively used.", + "Checks": [], + "Attributes": [ + { + "Section": "5 Policies", + "SubSection": "5.1 RBAC and Service Accounts", + "Profile": "Level 1", + "AssessmentStatus": "Manual", + "Description": "The `default` service account should not be used to ensure that rights granted to applications can be more easily audited and reviewed.", + "RationaleStatement": "Kubernetes provides a default service account which is used by cluster workloads where no specific service account is assigned to the pod. Where access to the Kubernetes API from a pod is required, a specific service account should be created for that pod, and rights granted to that service account. The default service account should be configured to ensure that it does not automatically provide a service account token, and it must not have any non-default role bindings or custom role assignments", + "ImpactStatement": "All workloads which require access to the Kubernetes API will require an explicit service account to be created.", + "RemediationProcedure": "Create explicit service accounts wherever a Kubernetes workload requires specific access to the Kubernetes API server. Modify the configuration of each default service account to include this value ``` automountServiceAccountToken: false ```", + "AuditProcedure": "For each namespace in the cluster, review the rights assigned to the default service account and ensure that it has no roles or cluster roles bound to it apart from the defaults. Additionally ensure that the `automountServiceAccountToken: false` setting is in place for each default service account.", + "AdditionalInformation": "", + "References": "https://kubernetes.io/docs/tasks/configure-pod-container/configure-service-account/", + "DefaultValue": "By default the `default` service account allows for its service account token to be mounted in pods in its namespace." + } + ] + }, + { + "Id": "5.1.6", + "Description": "Ensure that Service Account Tokens are only mounted where necessary", + "Checks": [], + "Attributes": [ + { + "Section": "5 Policies", + "SubSection": "5.1 RBAC and Service Accounts", + "Profile": "Level 1", + "AssessmentStatus": "Manual", + "Description": "Service accounts tokens should not be mounted in pods except where the workload running in the pod explicitly needs to communicate with the API server", + "RationaleStatement": "Mounting service account tokens inside pods can provide an avenue for privilege escalation attacks where an attacker is able to compromise a single pod in the cluster. Avoiding mounting these tokens removes this attack avenue.", + "ImpactStatement": "Pods mounted without service account tokens will not be able to communicate with the API server, except where the resource is available to unauthenticated principals.", + "RemediationProcedure": "Modify the definition of pods and service accounts which do not need to mount service account tokens to disable it.", + "AuditProcedure": "Review pod and service account objects in the cluster and ensure that the option below is set, unless the resource explicitly requires this access. ``` automountServiceAccountToken: false ```", + "AdditionalInformation": "", + "References": "https://kubernetes.io/docs/tasks/configure-pod-container/configure-service-account/", + "DefaultValue": "By default, all pods get a service account token mounted in them." + } + ] + }, + { + "Id": "5.1.7", + "Description": "Avoid use of system:masters group", + "Checks": [], + "Attributes": [ + { + "Section": "5 Policies", + "SubSection": "5.1 RBAC and Service Accounts", + "Profile": "Level 1", + "AssessmentStatus": "Manual", + "Description": "The special group `system:masters` should not be used to grant permissions to any user or service account, except where strictly necessary (e.g. bootstrapping access prior to RBAC being fully available)", + "RationaleStatement": "The `system:masters` group has unrestricted access to the Kubernetes API hard-coded into the API server source code. An authenticated user who is a member of this group cannot have their access reduced, even if all bindings and cluster role bindings which mention it, are removed. When combined with client certificate authentication, use of this group can allow for irrevocable cluster-admin level credentials to exist for a cluster.", + "ImpactStatement": "Once the RBAC system is operational in a cluster `system:masters` should not be specifically required, as ordinary bindings from principals to the `cluster-admin` cluster role can be made where unrestricted access is required.", + "RemediationProcedure": "Remove the `system:masters` group from all users in the cluster.", + "AuditProcedure": "Review a list of all credentials which have access to the cluster and ensure that the group `system:masters` is not used.", + "AdditionalInformation": "", + "References": "https://github.com/kubernetes/kubernetes/blob/master/pkg/registry/rbac/escalation_check.go#L38", + "DefaultValue": "By default some clusters will create a break glass client certificate which is a member of this group. Access to this client certificate should be carefully controlled and it should not be used for general cluster operations." + } + ] + }, + { + "Id": "5.1.8", + "Description": "Limit use of the Bind, Impersonate and Escalate permissions in the Kubernetes cluster", + "Checks": [], + "Attributes": [ + { + "Section": "5 Policies", + "SubSection": "5.1 RBAC and Service Accounts", + "Profile": "Level 1", + "AssessmentStatus": "Manual", + "Description": "Cluster roles and roles with the impersonate, bind or escalate permissions should not be granted unless strictly required. Each of these permissions allow a particular subject to escalate their privileges beyond those explicitly granted by cluster administrators", + "RationaleStatement": "The impersonate privilege allows a subject to impersonate other users gaining their rights to the cluster. The bind privilege allows the subject to add a binding to a cluster role or role which escalates their effective permissions in the cluster. The escalate privilege allows a subject to modify cluster roles to which they are bound, increasing their rights to that level. Each of these permissions has the potential to allow for privilege escalation to cluster-admin level.", + "ImpactStatement": "There are some cases where these permissions are required for cluster service operation, and care should be taken before removing these permissions from system service accounts.", + "RemediationProcedure": "Where possible, remove the impersonate, bind, and escalate rights from subjects.", + "AuditProcedure": "Review the users who have access to cluster roles or roles which provide the impersonate, bind, or escalate privileges.", + "AdditionalInformation": "", + "References": "https://raesene.github.io/blog/2020/12/12/Escalating_Away/:https://raesene.github.io/blog/2021/01/16/Getting-Into-A-Bind-with-Kubernetes/", + "DefaultValue": "In a default kubeadm cluster, the system:masters group and clusterrole-aggregation-controller service account have access to the escalate privilege. The system:masters group also has access to bind and impersonate." + } + ] + }, + { + "Id": "5.1.9", + "Description": "Minimize access to create persistent volumes", + "Checks": [ + "rbac_minimize_pv_creation_access" + ], + "Attributes": [ + { + "Section": "5 Policies", + "SubSection": "5.1 RBAC and Service Accounts", + "Profile": "Level 1", + "AssessmentStatus": "Manual", + "Description": "The ability to create persistent volumes in a cluster can provide an opportunity for privilege escalation, via the creation of `hostPath` volumes. As persistent volumes are not covered by Pod Security Admission, a user with access to create persistent volumes may be able to get access to sensitive files from the underlying host even where restrictive Pod Security Admission policies are in place.", + "RationaleStatement": "The ability to create persistent volumes in a cluster opens up possibilities for privilege escalation and should be restricted, where possible.", + "ImpactStatement": "", + "RemediationProcedure": "Where possible, remove `create` access to `PersistentVolume` objects in the cluster.", + "AuditProcedure": "Review the users who have create access to `PersistentVolume` objects in the Kubernetes API.", + "AdditionalInformation": "", + "References": "https://kubernetes.io/docs/concepts/security/rbac-good-practices/#persistent-volume-creation", + "DefaultValue": "" + } + ] + }, + { + "Id": "5.1.10", + "Description": "Minimize access to the proxy sub-resource of nodes", + "Checks": [ + "rbac_minimize_node_proxy_subresource_access" + ], + "Attributes": [ + { + "Section": "5 Policies", + "SubSection": "5.1 RBAC and Service Accounts", + "Profile": "Level 1", + "AssessmentStatus": "Manual", + "Description": "Users with access to the `Proxy` sub-resource of `Node` objects automatically have permissions to use the kubelet API, which may allow for privilege escalation or bypass cluster security controls such as audit logs. The kubelet provides an API which includes rights to execute commands in any container running on the node. Access to this API is covered by permissions to the main Kubernetes API via the `node` object. The proxy sub-resource specifically allows wide ranging access to the kubelet API. Direct access to the kubelet API bypasses controls like audit logging (there is no audit log of kubelet API access) and admission control.", + "RationaleStatement": "The ability to use the `proxy` sub-resource of `node` objects opens up possibilities for privilege escalation and should be restricted, where possible.", + "ImpactStatement": "", + "RemediationProcedure": "Where possible, remove access to the `proxy` sub-resource of `node` objects.", + "AuditProcedure": "Review the users who have access to the `proxy` sub-resource of `node` objects in the Kubernetes API.", + "AdditionalInformation": "", + "References": "https://kubernetes.io/docs/concepts/security/rbac-good-practices/#access-to-proxy-subresource-of-nodes:https://kubernetes.io/docs/reference/access-authn-authz/kubelet-authn-authz/#kubelet-authorization", + "DefaultValue": "" + } + ] + }, + { + "Id": "5.1.11", + "Description": "Minimize access to the approval sub-resource of certificatesigningrequests objects", + "Checks": [ + "rbac_minimize_csr_approval_access" + ], + "Attributes": [ + { + "Section": "5 Policies", + "SubSection": "5.1 RBAC and Service Accounts", + "Profile": "Level 1", + "AssessmentStatus": "Manual", + "Description": "Users with access to the update the `approval` sub-resource of `CertificateSigningRequests` objects can approve new client certificates for the Kubernetes API effectively allowing them to create new high-privileged user accounts. This can allow for privilege escalation to full cluster administrator, depending on users configured in the cluster", + "RationaleStatement": "The ability to update certificate signing requests should be limited.", + "ImpactStatement": "", + "RemediationProcedure": "Where possible, remove access to the `approval` sub-resource of `CertificateSigningRequests` objects.", + "AuditProcedure": "Review the users who have access to update the `approval` sub-resource of `CertificateSigningRequests` objects in the Kubernetes API.", + "AdditionalInformation": "", + "References": "https://kubernetes.io/docs/concepts/security/rbac-good-practices/#csrs-and-certificate-issuing", + "DefaultValue": "" + } + ] + }, + { + "Id": "5.1.12", + "Description": "Minimize access to webhook configuration objects", + "Checks": [ + "rbac_minimize_webhook_config_access" + ], + "Attributes": [ + { + "Section": "5 Policies", + "SubSection": "5.1 RBAC and Service Accounts", + "Profile": "Level 1", + "AssessmentStatus": "Manual", + "Description": "Users with rights to create/modify/delete `validatingwebhookconfigurations` or `mutatingwebhookconfigurations` can control webhooks that can read any object admitted to the cluster, and in the case of mutating webhooks, also mutate admitted objects. This could allow for privilege escalation or disruption of the operation of the cluster.", + "RationaleStatement": "The ability to manage webhook configuration should be limited", + "ImpactStatement": "", + "RemediationProcedure": "Where possible, remove access to the `validatingwebhookconfigurations` or `mutatingwebhookconfigurations` objects", + "AuditProcedure": "Review the users who have access to `validatingwebhookconfigurations` or `mutatingwebhookconfigurations` objects in the Kubernetes API.", + "AdditionalInformation": "", + "References": "https://kubernetes.io/docs/concepts/security/rbac-good-practices/#control-admission-webhooks", + "DefaultValue": "" + } + ] + }, + { + "Id": "5.1.13", + "Description": "Minimize access to the service account token creation", + "Checks": [ + "rbac_minimize_service_account_token_creation" + ], + "Attributes": [ + { + "Section": "5 Policies", + "SubSection": "5.1 RBAC and Service Accounts", + "Profile": "Level 1", + "AssessmentStatus": "Manual", + "Description": "Users with rights to create new service account tokens at a cluster level, can create long-lived privileged credentials in the cluster. This could allow for privilege escalation and persistent access to the cluster, even if the users account has been revoked.", + "RationaleStatement": "The ability to create service account tokens should be limited.", + "ImpactStatement": "", + "RemediationProcedure": "Where possible, remove access to the `token` sub-resource of `serviceaccount` objects.", + "AuditProcedure": "Review the users who have access to create the `token` sub-resource of `serviceaccount` objects in the Kubernetes API.", + "AdditionalInformation": "", + "References": "https://kubernetes.io/docs/concepts/security/rbac-good-practices/#token-request", + "DefaultValue": "" + } + ] + }, + { + "Id": "5.2.1", + "Description": "Ensure that the cluster has at least one active policy control mechanism in place", + "Checks": [], + "Attributes": [ + { + "Section": "5 Policies", + "SubSection": "5.2 Pod Security Standards", + "Profile": "Level 1", + "AssessmentStatus": "Manual", + "Description": "Every Kubernetes cluster should have at least one policy control mechanism in place to enforce the other requirements in this section. This could be the in-built Pod Security Admission controller, or a third party policy control system.", + "RationaleStatement": "Without an active policy control mechanism, it is not possible to limit the use of containers with access to underlying cluster nodes, via mechanisms like privileged containers, or the use of hostPath volume mounts.", + "ImpactStatement": "Where policy control systems are in place, there is a risk that workloads required for the operation of the cluster may be stopped from running. Care is required when implementing admission control policies to ensure that this does not occur.", + "RemediationProcedure": "Ensure that either Pod Security Admission or an external policy control system is in place for every namespace which contains user workloads.", + "AuditProcedure": "Review the workloads deployed to the cluster to understand if Pod Security Admission or external admission control systems are in place.", + "AdditionalInformation": "", + "References": "https://kubernetes.io/docs/concepts/security/pod-security-admission", + "DefaultValue": "By default, Pod Security Admission is enabled but no policies are in place." + } + ] + }, + { + "Id": "5.2.2", + "Description": "Minimize the admission of privileged containers", + "Checks": [ + "core_minimize_privileged_containers" + ], + "Attributes": [ + { + "Section": "5 Policies", + "SubSection": "5.2 Pod Security Standards", + "Profile": "Level 1", + "AssessmentStatus": "Manual", + "Description": "Do not generally permit containers to be run with the `securityContext.privileged` flag set to `true`.", + "RationaleStatement": "Privileged containers have access to all Linux Kernel capabilities and devices. A container running with full privileges can do almost everything that the host can do. This flag exists to allow special use-cases, like manipulating the network stack and accessing devices. There should be at least one admission control policy defined which does not permit privileged containers. If you need to run privileged containers, this should be defined in a separate policy and you should carefully check to ensure that only limited service accounts and users are given permission to use that policy.", + "ImpactStatement": "Pods defined with `spec.containers[].securityContext.privileged: true`, `spec.initContainers[].securityContext.privileged: true` and `spec.ephemeralContainers[].securityContext.privileged: true` will not be permitted.", + "RemediationProcedure": "Add policies to each namespace in the cluster which has user workloads to restrict the admission of privileged containers.", + "AuditProcedure": "Run the following command: `kubectl get pods -A -o=jsonpath='{range .items[*]}{@.metadata.name}: {@..securityContext}{end}'`It will produce an inventory of all the privileged use on the cluster, if any (please, refer to a sample below). Further grepping can be done to automate each specific violation detection. calico-kube-controllers-57b57c56f-jtmk4: {} << No Elevated Privileges calico-node-c4xv4: {} {privileged:true} {privileged:true} {privileged:true} {privileged:true} << Violates 5.2.2 dashboard-metrics-scraper-7bc864c59-2m2xw: {seccompProfile:{type:RuntimeDefault}} {allowPrivilegeEscalation:false,readOnlyRootFilesystem:true,runAsGroup:2001,runAsUser:1001}", + "AdditionalInformation": "", + "References": "https://kubernetes.io/docs/concepts/security/pod-security-standards/", + "DefaultValue": "By default, there are no restrictions on the creation of privileged containers." + } + ] + }, + { + "Id": "5.2.3", + "Description": "Minimize the admission of containers wishing to share the host process ID namespace", + "Checks": [ + "core_minimize_hostPID_containers" + ], + "Attributes": [ + { + "Section": "5 Policies", + "SubSection": "5.2 Pod Security Standards", + "Profile": "Level 1", + "AssessmentStatus": "Manual", + "Description": "Do not generally permit containers to be run with the `hostPID` flag set to true.", + "RationaleStatement": "A container running in the host's PID namespace can inspect processes running outside the container. If the container also has access to ptrace capabilities this can be used to escalate privileges outside of the container. There should be at least one admission control policy defined which does not permit containers to share the host PID namespace. If you need to run containers which require hostPID, this should be defined in a separate policy and you should carefully check to ensure that only limited service accounts and users are given permission to use that policy.", + "ImpactStatement": "Pods defined with `spec.hostPID: true` will not be permitted unless they are run under a specific policy.", + "RemediationProcedure": "Configure the Admission Controller to restrict the admission of `hostPID` containers.", + "AuditProcedure": "Fetch hostPID from each pod with `get pods -A -o=jsonpath='{range .items[*]}{@.metadata.name}: {@.spec.hostPID}{end}'`", + "AdditionalInformation": "", + "References": "https://kubernetes.io/docs/concepts/security/pod-security-standards/", + "DefaultValue": "By default, there are no restrictions on the creation of `hostPID` containers." + } + ] + }, + { + "Id": "5.2.4", + "Description": "Minimize the admission of containers wishing to share the host IPC namespace", + "Checks": [ + "core_minimize_hostIPC_containers" + ], + "Attributes": [ + { + "Section": "5 Policies", + "SubSection": "5.2 Pod Security Standards", + "Profile": "Level 1", + "AssessmentStatus": "Manual", + "Description": "Do not generally permit containers to be run with the `hostIPC` flag set to true.", + "RationaleStatement": "A container running in the host's IPC namespace can use IPC to interact with processes outside the container. There should be at least one admission control policy defined which does not permit containers to share the host IPC namespace. If you need to run containers which require hostIPC, this should be defined in a separate policy and you should carefully check to ensure that only limited service accounts and users are given permission to use that policy.", + "ImpactStatement": "Pods defined with `spec.hostIPC: true` will not be permitted unless they are run under a specific policy.", + "RemediationProcedure": "Add policies to each namespace in the cluster which has user workloads to restrict the admission of `hostIPC` containers.", + "AuditProcedure": "Fetch hostIPC from each pod with `get pods -A -o=jsonpath='{range .items[*]}{@.metadata.name}: {@.spec.hostIPC}{end}'`", + "AdditionalInformation": "", + "References": "https://kubernetes.io/docs/concepts/security/pod-security-standards/", + "DefaultValue": "By default, there are no restrictions on the creation of `hostIPC` containers." + } + ] + }, + { + "Id": "5.2.5", + "Description": "Minimize the admission of containers wishing to share the host network namespace", + "Checks": [ + "core_minimize_hostNetwork_containers" + ], + "Attributes": [ + { + "Section": "5 Policies", + "SubSection": "5.2 Pod Security Standards", + "Profile": "Level 1", + "AssessmentStatus": "Manual", + "Description": "Do not generally permit containers to be run with the `hostNetwork` flag set to true.", + "RationaleStatement": "A container running in the host's network namespace could access the local loopback device, and could access network traffic to and from other pods. There should be at least one admission control policy defined which does not permit containers to share the host network namespace. If you need to run containers which require access to the host's network namespaces, this should be defined in a separate policy and you should carefully check to ensure that only limited service accounts and users are given permission to use that policy.", + "ImpactStatement": "Pods defined with `spec.hostNetwork: true` will not be permitted unless they are run under a specific policy.", + "RemediationProcedure": "Add policies to each namespace in the cluster which has user workloads to restrict the admission of `hostNetwork` containers.", + "AuditProcedure": "Fetch hostNetwork from each pod with `get pods -A -o=jsonpath='{range .items[*]}{@.metadata.name}: {@.spec.hostNetwork}{end}'`", + "AdditionalInformation": "", + "References": "https://kubernetes.io/docs/concepts/security/pod-security-standards/", + "DefaultValue": "By default, there are no restrictions on the creation of `hostNetwork` containers." + } + ] + }, + { + "Id": "5.2.6", + "Description": "Minimize the admission of containers with allowPrivilegeEscalation", + "Checks": [ + "core_minimize_allowPrivilegeEscalation_containers" + ], + "Attributes": [ + { + "Section": "5 Policies", + "SubSection": "5.2 Pod Security Standards", + "Profile": "Level 1", + "AssessmentStatus": "Manual", + "Description": "Do not generally permit containers to be run with the `allowPrivilegeEscalation` flag set to true. Allowing this right can lead to a process running a container getting more rights than it started with. It's important to note that these rights are still constrained by the overall container sandbox, and this setting does not relate to the use of privileged containers.", + "RationaleStatement": "A container running with the `allowPrivilegeEscalation` flag set to `true` may have processes that can gain more privileges than their parent. There should be at least one admission control policy defined which does not permit containers to allow privilege escalation. The option exists (and is defaulted to true) to permit setuid binaries to run. If you have need to run containers which use setuid binaries or require privilege escalation, this should be defined in a separate policy and you should carefully check to ensure that only limited service accounts and users are given permission to use that policy.", + "ImpactStatement": "Pods defined with `securityContext: allowPrivilegeEscalation: true` will not be permitted unless they are run under a specific policy.", + "RemediationProcedure": "Add policies to each namespace in the cluster which has user workloads to restrict the admission of containers with `securityContext: allowPrivilegeEscalation: true`", + "AuditProcedure": "List the policies in use for each namespace in the cluster, ensure that each policy disallows the admission of containers which allow privilege escalation. To fetch a list of pods which `allowPrivilegeEscalation` run this command: `get pods -A -o=jsonpath='{range .items[*]}{@.metadata.name}: {@..securityContext}{end}'`", + "AdditionalInformation": "", + "References": "https://kubernetes.io/docs/concepts/security/pod-security-standards/", + "DefaultValue": "By default, there are no restrictions on contained process ability to escalate privileges, within the context of the container." + } + ] + }, + { + "Id": "5.2.7", + "Description": "Minimize the admission of root containers", + "Checks": [ + "core_minimize_root_containers_admission" + ], + "Attributes": [ + { + "Section": "5 Policies", + "SubSection": "5.2 Pod Security Standards", + "Profile": "Level 2", + "AssessmentStatus": "Manual", + "Description": "Do not generally permit containers to be run as the root user.", + "RationaleStatement": "Containers may run as any Linux user. Containers which run as the root user, whilst constrained by Container Runtime security features still have a escalated likelihood of container breakout. Ideally, all containers should run as a defined non-UID 0 user. There should be at least one admission control policy defined which does not permit root containers. If you need to run root containers, this should be defined in a separate policy and you should carefully check to ensure that only limited service accounts and users are given permission to use that policy.", + "ImpactStatement": "Pods with containers which run as the root user will not be permitted.", + "RemediationProcedure": "Create a policy for each namespace in the cluster, ensuring that either `MustRunAsNonRoot` or `MustRunAs` with the range of UIDs not including 0, is set.", + "AuditProcedure": "List the policies in use for each namespace in the cluster, ensure that each policy restricts the use of root containers by setting `MustRunAsNonRoot` or `MustRunAs` with the range of UIDs not including 0.", + "AdditionalInformation": "", + "References": "https://kubernetes.io/docs/concepts/security/pod-security-standards/", + "DefaultValue": "By default, there are no restrictions on the use of root containers and if a User is not specified in the image, the container will run as root." + } + ] + }, + { + "Id": "5.2.8", + "Description": "Minimize the admission of containers with the NET_RAW capability", + "Checks": [ + "core_minimize_net_raw_capability_admission" + ], + "Attributes": [ + { + "Section": "5 Policies", + "SubSection": "5.2 Pod Security Standards", + "Profile": "Level 1", + "AssessmentStatus": "Manual", + "Description": "Do not generally permit containers with the potentially dangerous NET_RAW capability.", + "RationaleStatement": "Containers run with a default set of capabilities as assigned by the Container Runtime. By default this can include potentially dangerous capabilities. With Docker as the container runtime the NET_RAW capability is enabled which may be misused by malicious containers. Ideally, all containers should drop this capability. There should be at least one admission control policy defined which does not permit containers with the NET_RAW capability. If you need to run containers with this capability, this should be defined in a separate policy and you should carefully check to ensure that only limited service accounts and users are given permission to use that policy.", + "ImpactStatement": "Pods with containers which run with the NET_RAW capability will not be permitted.", + "RemediationProcedure": "Add policies to each namespace in the cluster which has user workloads to restrict the admission of containers with the `NET_RAW` capability.", + "AuditProcedure": "List the policies in use for each namespace in the cluster, ensure that at least one policy disallows the admission of containers with the `NET_RAW` capability.", + "AdditionalInformation": "", + "References": "https://kubernetes.io/docs/concepts/security/pod-security-standards/:https://www.nccgroup.trust/uk/our-research/abusing-privileged-and-unprivileged-linux-containers/", + "DefaultValue": "By default, there are no restrictions on the creation of containers with the `NET_RAW` capability." + } + ] + }, + { + "Id": "5.2.9", + "Description": "Minimize the admission of containers with capabilities assigned", + "Checks": [ + "core_minimize_containers_capabilities_assigned" + ], + "Attributes": [ + { + "Section": "5 Policies", + "SubSection": "5.2 Pod Security Standards", + "Profile": "Level 1", + "AssessmentStatus": "Manual", + "Description": "Do not generally permit containers with capabilities assigned beyond the default set.", + "RationaleStatement": "Containers run with a default set of capabilities as assigned by the Container Runtime. Capabilities outside this set can be added to containers which could expose them to risks of container breakout attacks. There should be at least one policy defined which prevents containers with capabilities beyond the default set from launching. If you need to run containers with additional capabilities, this should be defined in a separate policy and you should carefully check to ensure that only limited service accounts and users are given permission to use that policy.", + "ImpactStatement": "Pods with containers which require capabilities outside the default set will not be permitted.", + "RemediationProcedure": "Ensure that `allowedCapabilities` is not present in policies for the cluster unless it is set to an empty array.", + "AuditProcedure": "Ensure that allowedCapabilities is not present in policies for the cluster unless it is set to an empty array. Run: kubectl get pods -A -o=jsonpath='{range .items[*]}{@.metadata.name}: {@..securityContext}{end}'", + "AdditionalInformation": "", + "References": "https://kubernetes.io/docs/concepts/security/pod-security-standards/:https://www.nccgroup.trust/uk/our-research/abusing-privileged-and-unprivileged-linux-containers/", + "DefaultValue": "By default, there are no restrictions on adding capabilities to containers." + } + ] + }, + { + "Id": "5.2.10", + "Description": "Minimize the admission of Windows HostProcess Containers", + "Checks": [ + "core_minimize_admission_windows_hostprocess_containers" + ], + "Attributes": [ + { + "Section": "5 Policies", + "SubSection": "5.2 Pod Security Standards", + "Profile": "Level 1", + "AssessmentStatus": "Manual", + "Description": "Do not generally permit Windows containers to be run with the `hostProcess` flag set to true.", + "RationaleStatement": "A Windows container making use of the `hostProcess` flag can interact with the underlying Windows cluster node. As per the Kubernetes documentation, this provides privileged access to the Windows node. Where Windows containers are used inside a Kubernetes cluster, there should be at least one admission control policy which does not permit `hostProcess` Windows containers. If you need to run Windows containers which require `hostProcess`, this should be defined in a separate policy and you should carefully check to ensure that only limited service accounts and users are given permission to use that policy.", + "ImpactStatement": "Pods defined with `securityContext.windowsOptions.hostProcess: true` will not be permitted unless they are run under a specific policy.", + "RemediationProcedure": "Add policies to each namespace in the cluster which has user workloads to restrict the admission of `hostProcess` containers.", + "AuditProcedure": "List the policies in use for each namespace in the cluster, ensure that each policy disallows the admission of `hostProcess` containers", + "AdditionalInformation": "", + "References": "https://kubernetes.io/docs/tasks/configure-pod-container/create-hostprocess-pod/:https://kubernetes.io/docs/concepts/security/pod-security-standards/", + "DefaultValue": "By default, there are no restrictions on the creation of `hostProcess` containers." + } + ] + }, + { + "Id": "5.2.11", + "Description": "Minimize the admission of HostPath volumes", + "Checks": [], + "Attributes": [ + { + "Section": "5 Policies", + "SubSection": "5.2 Pod Security Standards", + "Profile": "Level 1", + "AssessmentStatus": "Manual", + "Description": "Do not generally admit containers which make use of `hostPath` volumes.", + "RationaleStatement": "A container which mounts a `hostPath` volume as part of its specification will have access to the filesystem of the underlying cluster node. The use of `hostPath` volumes may allow containers access to privileged areas of the node filesystem. There should be at least one admission control policy defined which does not permit containers to mount `hostPath` volumes. If you need to run containers which require `hostPath` volumes, this should be defined in a separate policy and you should carefully check to ensure that only limited service accounts and users are given permission to use that policy.", + "ImpactStatement": "Pods defined which make use of `hostPath` volumes will not be permitted unless they are run under a specific policy.", + "RemediationProcedure": "Add policies to each namespace in the cluster which has user workloads to restrict the admission of containers which use `hostPath` volumes.", + "AuditProcedure": "List the policies in use for each namespace in the cluster, ensure that each policy disallows the admission of containers with `hostPath` volumes.", + "AdditionalInformation": "", + "References": "https://kubernetes.io/docs/concepts/security/pod-security-standards/", + "DefaultValue": "By default, there are no restrictions on the creation of `hostPath` volumes." + } + ] + }, + { + "Id": "5.2.12", + "Description": "Minimize the admission of containers which use HostPorts", + "Checks": [ + "core_minimize_admission_hostport_containers" + ], + "Attributes": [ + { + "Section": "5 Policies", + "SubSection": "5.2 Pod Security Standards", + "Profile": "Level 1", + "AssessmentStatus": "Manual", + "Description": "Do not generally permit containers which require the use of HostPorts.", + "RationaleStatement": "Host ports connect containers directly to the host's network. This can bypass controls such as network policy. There should be at least one admission control policy defined which does not permit containers which require the use of HostPorts. If you need to run containers which require HostPorts, this should be defined in a separate policy and you should carefully check to ensure that only limited service accounts and users are given permission to use that policy.", + "ImpactStatement": "Pods defined with `hostPort` settings in either the container, initContainer or ephemeralContainer sections will not be permitted unless they are run under a specific policy.", + "RemediationProcedure": "Add policies to each namespace in the cluster which has user workloads to restrict the admission of containers which use `hostPort` sections.", + "AuditProcedure": "List the policies in use for each namespace in the cluster, ensure that each policy disallows the admission of containers which have `hostPort` sections.", + "AdditionalInformation": "", + "References": "https://kubernetes.io/docs/concepts/security/pod-security-standards/", + "DefaultValue": "By default, there are no restrictions on the use of HostPorts." + } + ] + }, + { + "Id": "5.3.1", + "Description": "Ensure that the CNI in use supports Network Policies", + "Checks": [], + "Attributes": [ + { + "Section": "5 Policies", + "SubSection": "5.3 Network Policies and CNI", + "Profile": "Level 1", + "AssessmentStatus": "Manual", + "Description": "There are a variety of CNI plugins available for Kubernetes. If the CNI in use does not support Network Policies it may not be possible to effectively restrict traffic in the cluster.", + "RationaleStatement": "Kubernetes network policies are enforced by the CNI plugin in use. As such it is important to ensure that the CNI plugin supports both Ingress and Egress network policies.", + "ImpactStatement": "None", + "RemediationProcedure": "If the CNI plugin in use does not support network policies, consideration should be given to making use of a different plugin, or finding an alternate mechanism for restricting traffic in the Kubernetes cluster.", + "AuditProcedure": "Review the documentation of CNI plugin in use by the cluster, and confirm that it supports Ingress and Egress network policies.", + "AdditionalInformation": "One example here is Flannel (https://github.com/coreos/flannel) which does not support Network policy unless Calico is also in use.", + "References": "https://kubernetes.io/docs/concepts/extend-kubernetes/compute-storage-net/network-plugins/", + "DefaultValue": "This will depend on the CNI plugin in use." + } + ] + }, + { + "Id": "5.3.2", + "Description": "Ensure that all Namespaces have Network Policies defined", + "Checks": [], + "Attributes": [ + { + "Section": "5 Policies", + "SubSection": "5.3 Network Policies and CNI", + "Profile": "Level 2", + "AssessmentStatus": "Manual", + "Description": "Use network policies to isolate traffic in your cluster network.", + "RationaleStatement": "Running different applications on the same Kubernetes cluster creates a risk of one compromised application attacking a neighboring application. Network segmentation is important to ensure that containers can communicate only with those they are supposed to. A network policy is a specification of how selections of pods are allowed to communicate with each other and other network endpoints. Network Policies are namespace scoped. When a network policy is introduced to a given namespace, all traffic not allowed by the policy is denied. However, if there are no network policies in a namespace all traffic will be allowed into and out of the pods in that namespace.", + "ImpactStatement": "Once network policies are in use within a given namespace, traffic not explicitly allowed by a network policy will be denied. As such it is important to ensure that, when introducing network policies, legitimate traffic is not blocked.", + "RemediationProcedure": "Follow the documentation and create `NetworkPolicy` objects as you need them.", + "AuditProcedure": "Run the below command and review the `NetworkPolicy` objects created in the cluster. ``` kubectl get networkpolicy --all-namespaces ``` Ensure that each namespace defined in the cluster has at least one Network Policy.", + "AdditionalInformation": "", + "References": "https://kubernetes.io/docs/concepts/services-networking/networkpolicies/:https://octetz.com/posts/k8s-network-policy-apis:https://kubernetes.io/docs/tasks/configure-pod-container/declare-network-policy/", + "DefaultValue": "By default, network policies are not created." + } + ] + }, + { + "Id": "5.4.1", + "Description": "Prefer using secrets as files over secrets as environment variables", + "Checks": [ + "core_no_secrets_envs" + ], + "Attributes": [ + { + "Section": "5 Policies", + "SubSection": "5.4 Secrets Management", + "Profile": "Level 2", + "AssessmentStatus": "Manual", + "Description": "Kubernetes supports mounting secrets as data volumes or as environment variables. Minimize the use of environment variable secrets.", + "RationaleStatement": "It is reasonably common for application code to log out its environment (particularly in the event of an error). This will include any secret values passed in as environment variables, so secrets can easily be exposed to any user or entity who has access to the logs.", + "ImpactStatement": "Application code which expects to read secrets in the form of environment variables would need modification", + "RemediationProcedure": "If possible, rewrite application code to read secrets from mounted secret files, rather than from environment variables.", + "AuditProcedure": "Run the following command to find references to objects which use environment variables defined from secrets. ``` kubectl get all -o jsonpath='{range .items[?(@..secretKeyRef)]}{.kind}{@.metadata.name}{end}' -A ```", + "AdditionalInformation": "Mounting secrets as volumes has the additional benefit that secret values can be updated without restarting the pod", + "References": "https://kubernetes.io/docs/concepts/configuration/secret/#using-secrets", + "DefaultValue": "By default, secrets are not defined" + } + ] + }, + { + "Id": "5.4.2", + "Description": "Consider external secret storage", + "Checks": [], + "Attributes": [ + { + "Section": "5 Policies", + "SubSection": "5.4 Secrets Management", + "Profile": "Level 2", + "AssessmentStatus": "Manual", + "Description": "Consider the use of an external secrets storage and management system, instead of using Kubernetes Secrets directly, if you have more complex secret management needs. Ensure the solution requires authentication to access secrets, has auditing of access to and use of secrets, and encrypts secrets. Some solutions also make it easier to rotate secrets.", + "RationaleStatement": "Kubernetes supports secrets as first-class objects, but care needs to be taken to ensure that access to secrets is carefully limited. Using an external secrets provider can ease the management of access to secrets, especially where secrests are used across both Kubernetes and non-Kubernetes environments.", + "ImpactStatement": "None", + "RemediationProcedure": "Refer to the secrets management options offered by your cloud provider or a third-party secrets management solution.", + "AuditProcedure": "Review your secrets management implementation.", + "AdditionalInformation": "", + "References": "", + "DefaultValue": "By default, no external secret management is configured." + } + ] + }, + { + "Id": "5.5.1", + "Description": "Configure Image Provenance using ImagePolicyWebhook admission controller", + "Checks": [], + "Attributes": [ + { + "Section": "5 Policies", + "SubSection": "5.5 Extensible Admission Control", + "Profile": "Level 2", + "AssessmentStatus": "Manual", + "Description": "Configure Image Provenance for your deployment.", + "RationaleStatement": "Kubernetes supports plugging in provenance rules to accept or reject the images in your deployments. You could configure such rules to ensure that only approved images are deployed in the cluster.", + "ImpactStatement": "You need to regularly maintain your provenance configuration based on container image updates.", + "RemediationProcedure": "Follow the Kubernetes documentation and setup image provenance.", + "AuditProcedure": "Review the pod definitions in your cluster and verify that image provenance is configured as appropriate.", + "AdditionalInformation": "", + "References": "https://kubernetes.io/docs/admin/admission-controllers/#imagepolicywebhook:https://github.com/kubernetes/community/blob/master/contributors/design-proposals/image-provenance.md:https://hub.docker.com/r/dnurmi/anchore-toolbox/:https://github.com/kubernetes/kubernetes/issues/22888", + "DefaultValue": "By default, image provenance is not set." + } + ] + }, + { + "Id": "5.6.1", + "Description": "Create administrative boundaries between resources using namespaces", + "Checks": [], + "Attributes": [ + { + "Section": "5 Policies", + "SubSection": "5.6 General Policies", + "Profile": "Level 1", + "AssessmentStatus": "Manual", + "Description": "Use namespaces to isolate your Kubernetes objects.", + "RationaleStatement": "Limiting the scope of user permissions can reduce the impact of mistakes or malicious activities. A Kubernetes namespace allows you to partition created resources into logically named groups. Resources created in one namespace can be hidden from other namespaces. By default, each resource created by a user in Kubernetes cluster runs in a default namespace, called `default`. You can create additional namespaces and attach resources and users to them. You can use Kubernetes Authorization plugins to create policies that segregate access to namespace resources between different users.", + "ImpactStatement": "You need to switch between namespaces for administration.", + "RemediationProcedure": "Follow the documentation and create namespaces for objects in your deployment as you need them.", + "AuditProcedure": "Run the below command and review the namespaces created in the cluster. ``` kubectl get namespaces ``` Ensure that these namespaces are the ones you need and are adequately administered as per your requirements.", + "AdditionalInformation": "", + "References": "https://kubernetes.io/docs/concepts/overview/working-with-objects/namespaces/#viewing-namespaces:http://blog.kubernetes.io/2016/08/security-best-practices-kubernetes-deployment.html:https://github.com/kubernetes/enhancements/tree/master/keps/sig-node/589-efficient-node-heartbeats", + "DefaultValue": "By default, Kubernetes starts with 4 initial namespaces: 1. `default` - The default namespace for objects with no other namespace 1. `kube-system` - The namespace for objects created by the Kubernetes system 1. `kube-node-lease` - Namespace used for node heartbeats 1. `kube-public` - Namespace used for public information in a cluster" + } + ] + }, + { + "Id": "5.6.2", + "Description": "Ensure that the seccomp profile is set to docker/default in your pod definitions", + "Checks": [ + "core_seccomp_profile_docker_default" + ], + "Attributes": [ + { + "Section": "5 Policies", + "SubSection": "5.6 General Policies", + "Profile": "Level 2", + "AssessmentStatus": "Manual", + "Description": "Enable `docker/default` seccomp profile in your pod definitions.", + "RationaleStatement": "Seccomp (secure computing mode) is used to restrict the set of system calls applications can make, allowing cluster administrators greater control over the security of workloads running in the cluster. Kubernetes disables seccomp profiles by default for historical reasons. You should enable it to ensure that the workloads have restricted actions available within the container.", + "ImpactStatement": "If the `docker/default` seccomp profile is too restrictive for you, you would have to create/manage your own seccomp profiles.", + "RemediationProcedure": "Use security context to enable the `docker/default` seccomp profile in your pod definitions. An example is as below: ``` securityContext: seccompProfile: type: RuntimeDefault ```", + "AuditProcedure": "Review the pod definitions in your cluster. It should create a line as below: ``` securityContext: seccompProfile: type: RuntimeDefault ```", + "AdditionalInformation": "", + "References": "https://kubernetes.io/docs/tutorials/clusters/seccomp/:https://docs.docker.com/engine/security/seccomp/", + "DefaultValue": "By default, seccomp profile is set to `unconfined` which means that no seccomp profiles are enabled." + } + ] + }, + { + "Id": "5.6.3", + "Description": "Apply Security Context to Your Pods and Containers", + "Checks": [], + "Attributes": [ + { + "Section": "5 Policies", + "SubSection": "5.6 General Policies", + "Profile": "Level 2", + "AssessmentStatus": "Manual", + "Description": "Apply Security Context to Your Pods and Containers", + "RationaleStatement": "A security context defines the operating system security settings (uid, gid, capabilities, SELinux role, etc..) applied to a container. When designing your containers and pods, make sure that you configure the security context for your pods, containers, and volumes. A security context is a property defined in the deployment yaml. It controls the security parameters that will be assigned to the pod/container/volume. There are two levels of security context: pod level security context, and container level security context.", + "ImpactStatement": "If you incorrectly apply security contexts, you may have trouble running the pods.", + "RemediationProcedure": "Follow the Kubernetes documentation and apply security contexts to your pods. For a suggested list of security contexts, you may refer to the CIS Security Benchmark for Docker Containers.", + "AuditProcedure": "Review the pod definitions in your cluster and verify that you have security contexts defined as appropriate.", + "AdditionalInformation": "", + "References": "https://kubernetes.io/docs/concepts/policy/security-context/:https://learn.cisecurity.org/benchmarks", + "DefaultValue": "By default, no security contexts are automatically applied to pods." + } + ] + }, + { + "Id": "5.6.4", + "Description": "The default namespace should not be used", + "Checks": [], + "Attributes": [ + { + "Section": "5 Policies", + "SubSection": "5.6 General Policies", + "Profile": "Level 2", + "AssessmentStatus": "Manual", + "Description": "Kubernetes provides a default namespace, where objects are placed if no namespace is specified for them. Placing objects in this namespace makes application of RBAC and other controls more difficult.", + "RationaleStatement": "Resources in a Kubernetes cluster should be segregated by namespace, to allow for security controls to be applied at that level and to make it easier to manage resources.", + "ImpactStatement": "None", + "RemediationProcedure": "Ensure that namespaces are created to allow for appropriate segregation of Kubernetes resources and that all new resources are created in a specific namespace.", + "AuditProcedure": "Run this command to list objects in default namespace ``` kubectl get $(kubectl api-resources --verbs=list --namespaced=true -o name | paste -sd, -) --ignore-not-found -n default ``` The only entries there should be system managed resources such as the `kubernetes` service", + "AdditionalInformation": "", + "References": "", + "DefaultValue": "Unless a namespace is specific on object creation, the `default` namespace will be used" + } + ] + } + ] +} diff --git a/prowler/compliance/m365/cis_6.0_m365.json b/prowler/compliance/m365/cis_6.0_m365.json new file mode 100644 index 0000000000..ea97cf10e6 --- /dev/null +++ b/prowler/compliance/m365/cis_6.0_m365.json @@ -0,0 +1,3087 @@ +{ + "Framework": "CIS", + "Name": "CIS Microsoft 365 Foundations Benchmark v6.0.0", + "Version": "6.0", + "Provider": "M365", + "Description": "The CIS Microsoft 365 Foundations Benchmark provides prescriptive guidance for establishing a secure configuration posture for Microsoft 365 Cloud offerings running on any OS. This guide includes recommendations for Exchange Online, SharePoint Online, OneDrive for Business, Teams, Power BI (Fabric) and Microsoft Entra ID.", + "Requirements": [ + { + "Id": "1.1.1", + "Description": "Administrative accounts are special privileged accounts that could have varying levels of access to data, users, and settings. Regular user accounts should never be utilized for administrative tasks and care should be taken, in the case of a hybrid environment, to keep administrative accounts separate from on-prem accounts. Administrative accounts should not have applications assigned so that they have no access to potentially vulnerable services (EX. email, Teams, SharePoint, etc.) and only access to perform tasks as needed for administrative purposes. Ensure administrative accounts are not On-premises sync enabled.", + "Checks": [ + "entra_admin_users_cloud_only" + ], + "Attributes": [ + { + "Section": "1 Microsoft 365 admin center", + "SubSection": "1.1 Users", + "Profile": "E3 Level 1", + "AssessmentStatus": "Automated", + "Description": "Administrative accounts are special privileged accounts that could have varying levels of access to data, users, and settings. Regular user accounts should never be utilized for administrative tasks and care should be taken, in the case of a hybrid environment, to keep administrative accounts separate from on-prem accounts. Administrative accounts should not have applications assigned so that they have no access to potentially vulnerable services (EX. email, Teams, SharePoint, etc.) and only access to perform tasks as needed for administrative purposes. Ensure administrative accounts are not On-premises sync enabled.", + "RationaleStatement": "In a hybrid environment, having separate accounts will help ensure that in the event of a breach in the cloud, that the breach does not affect the on-prem environment and vice versa.", + "ImpactStatement": "Administrative users will need to utilize login/logout functionality to switch accounts when performing administrative tasks, which means they will not benefit from SSO. This will require a migration process from the 'daily driver' account to a dedicated admin account. Once the new admin account is created, permission sets should be migrated from the 'daily driver' account to the new admin account. This includes both M365 and Azure RBAC roles. Failure to migrate Azure RBAC roles could prevent an admin from seeing their subscriptions/resources while using their admin account.", + "RemediationProcedure": "Remediation will require first identifying the privileged accounts that are synced from on-premises and then creating a new cloud-only account for that user. Once a replacement account is established, the hybrid account should have its role reduced to that of a non-privileged user or removed depending on the need.", + "AuditProcedure": "To audit using the UI: 1. Navigate to Microsoft Entra admin center https://entra.microsoft.com/. 2. Click to expand Identity > Users and select All users. 3. To the right of the search box click the Add filter button. 4. Add the On-premises sync enabled filter with the value set to Yes and click Apply. 5. Verify that no user accounts in administrative roles are present in the filtered list.", + "AdditionalInformation": "", + "References": "https://learn.microsoft.com/en-us/microsoft-365/admin/add-users/add-users?view=o365-worldwide:https://learn.microsoft.com/en-us/microsoft-365/enterprise/protect-your-global-administrator-accounts?view=o365-worldwide:https://learn.microsoft.com/en-us/entra/identity/role-based-access-control/best-practices#9-use-cloud-native-accounts-for-microsoft-entra-roles:https://learn.microsoft.com/en-us/entra/fundamentals/whatis:https://learn.microsoft.com/en-us/entra/identity/role-based-access-control/permissions-reference", + "DefaultValue": "N/A" + } + ] + }, + { + "Id": "1.1.2", + "Description": "Emergency access or 'break glass' accounts are limited for emergency scenarios where normal administrative accounts are unavailable. They are not assigned to a specific user and will have a combination of physical and technical controls to prevent them from being accessed outside a true emergency. Ensure two Emergency Access accounts have been defined.", + "Checks": [], + "Attributes": [ + { + "Section": "1 Microsoft 365 admin center", + "SubSection": "1.1 Users", + "Profile": "E3 Level 1", + "AssessmentStatus": "Manual", + "Description": "Emergency access or 'break glass' accounts are limited for emergency scenarios where normal administrative accounts are unavailable. They are not assigned to a specific user and will have a combination of physical and technical controls to prevent them from being accessed outside a true emergency. Ensure two Emergency Access accounts have been defined.", + "RationaleStatement": "In various situations, an organization may require the use of a break glass account to gain emergency access. In the event of losing access to administrative functions, an organization may experience a significant loss in its ability to provide support, lose insight into its security posture, and potentially suffer financial losses.", + "ImpactStatement": "If care is not taken in properly implementing an emergency access account this could weaken security posture. Microsoft recommends excluding at least one of these accounts from all conditional access rules therefore passwords must have sufficient entropy and length to protect against random guesses. FIDO2 security keys may be used instead of a password for secure passwordless solution.", + "RemediationProcedure": "Step 1 - Create two emergency access accounts. Step 2 - Exclude at least one account from conditional access policies. Step 3 - Ensure the necessary procedures and policies are in place.", + "AuditProcedure": "Step 1 - Ensure a policy and procedure is in place at the organization. Step 2 - Ensure two emergency access accounts are defined. Step 3 - Ensure at least one account is excluded from all conditional access rules.", + "AdditionalInformation": "Microsoft has additional instructions regarding using Azure Monitor to capture events in the Log Analytics workspace, and then generate alerts for Emergency Access accounts.", + "References": "https://learn.microsoft.com/en-us/entra/identity/role-based-access-control/security-planning#stage-1-critical-items-to-do-right-now:https://learn.microsoft.com/en-us/entra/identity/role-based-access-control/security-emergency-access:https://learn.microsoft.com/en-us/entra/identity/role-based-access-control/admin-units-restricted-management:https://learn.microsoft.com/en-us/entra/identity/authentication/concept-mandatory-multifactor-authentication#accounts", + "DefaultValue": "Not defined." + } + ] + }, + { + "Id": "1.1.3", + "Description": "More than one global administrator should be designated so a single admin can be monitored and to provide redundancy should a single admin leave an organization. Additionally, there should be no more than four global admins set for any tenant. Ideally global administrators will have no licenses assigned to them.", + "Checks": [ + "admincenter_users_between_two_and_four_global_admins" + ], + "Attributes": [ + { + "Section": "1 Microsoft 365 admin center", + "SubSection": "1.1 Users", + "Profile": "E3 Level 1", + "AssessmentStatus": "Automated", + "Description": "More than one global administrator should be designated so a single admin can be monitored and to provide redundancy should a single admin leave an organization. Additionally, there should be no more than four global admins set for any tenant. Ideally global administrators will have no licenses assigned to them.", + "RationaleStatement": "If there is only one global tenant administrator, he or she can perform malicious activity without the possibility of being discovered by another admin. If there are numerous global tenant administrators, the more likely it is that one of their accounts will be successfully breached by an external attacker.", + "ImpactStatement": "The potential impact associated with ensuring compliance with this requirement is dependent upon the current number of global administrators configured in the tenant. If there is only one global administrator in a tenant, an additional global administrator will need to be identified and configured. If there are more than four global administrators, a review of role requirements for current global administrators will be required to identify which of the users require global administrator access.", + "RemediationProcedure": "To remediate using the UI: 1. Navigate to the Microsoft 365 admin center https://admin.microsoft.com 2. Select Users > Active Users. 3. In the Search field enter the name of the user to be made a Global Administrator. 4. To create a new Global Admin or remove Global Admins follow the appropriate steps.", + "AuditProcedure": "To audit using the UI: 1. Navigate to the Microsoft 365 admin center https://admin.microsoft.com 2. Select Users > Active Users. 3. Select Filter then select Global Admins. 4. Review the list of Global Admins to confirm there are from two to four such accounts.", + "AdditionalInformation": "", + "References": "https://learn.microsoft.com/en-us/powershell/module/microsoft.graph.identity.directorymanagement/get-mgdirectoryrole?view=graph-powershell-1.0:https://learn.microsoft.com/en-us/entra/identity/role-based-access-control/permissions-reference#all-roles", + "DefaultValue": "" + } + ] + }, + { + "Id": "1.1.4", + "Description": "Administrative accounts are special privileged accounts that could have varying levels of access to data, users, and settings. A license can enable an account to gain access to a variety of different applications, depending on the license assigned. The recommended state is to not license a privileged account or use Microsoft Entra ID P1 or Microsoft Entra ID P2 licenses.", + "Checks": [ + "admincenter_users_admins_reduced_license_footprint" + ], + "Attributes": [ + { + "Section": "1 Microsoft 365 admin center", + "SubSection": "1.1 Users", + "Profile": "E3 Level 1", + "AssessmentStatus": "Automated", + "Description": "Administrative accounts are special privileged accounts that could have varying levels of access to data, users, and settings. A license can enable an account to gain access to a variety of different applications, depending on the license assigned. The recommended state is to not license a privileged account or use Microsoft Entra ID P1 or Microsoft Entra ID P2 licenses.", + "RationaleStatement": "Ensuring administrative accounts do not use licenses with applications assigned to them will reduce the attack surface of high privileged identities in the organization's environment. Granting access to a mailbox or other collaborative tools increases the likelihood that privileged users might interact with these applications, raising the risk of exposure to social engineering attacks or malicious content. These activities should be restricted to an unprivileged 'daily driver' account.", + "ImpactStatement": "Administrative users will have to switch accounts and utilize login/logout functionality when performing administrative tasks, as well as not benefiting from SSO.", + "RemediationProcedure": "To remediate using the UI: 1. Navigate to Microsoft 365 admin center https://admin.microsoft.com. 2. Click to expand Users select Active users 3. Click Add a user. 4. Fill out the appropriate fields for Name, user, etc. 5. When prompted to assign licenses select as needed Microsoft Entra ID P1 or Microsoft Entra ID P2, then click Next. 6. Under the Option settings screen you may choose from several types of privileged roles. Choose Admin center access followed by the appropriate role then click Next. 7. Select Finish adding.", + "AuditProcedure": "To audit using the UI: 1. Navigate to Microsoft 365 admin center https://admin.microsoft.com. 2. Click to expand Users select Active users. 3. Sort by the Licenses column. 4. For each user account in an administrative role verify the account is assigned a license that is not associated with applications i.e. (Microsoft Entra ID P1, Microsoft Entra ID P2).", + "AdditionalInformation": "", + "References": "https://learn.microsoft.com/en-us/microsoft-365/admin/add-users/add-users?view=o365-worldwide:https://learn.microsoft.com/en-us/microsoft-365/enterprise/protect-your-global-administrator-accounts?view=o365-worldwide:https://learn.microsoft.com/en-us/entra/fundamentals/whatis:https://learn.microsoft.com/en-us/entra/identity/role-based-access-control/permissions-reference", + "DefaultValue": "N/A" + } + ] + }, + { + "Id": "1.2.1", + "Description": "Microsoft 365 Groups is the foundational membership service that drives all teamwork across Microsoft 365. With Microsoft 365 Groups, you can give a group of people access to a collection of shared resources. While there are several different group types this recommendation concerns Microsoft 365 Groups. In the Administration panel, when a group is created, the default privacy value is 'Public'.", + "Checks": [ + "admincenter_groups_not_public_visibility" + ], + "Attributes": [ + { + "Section": "1 Microsoft 365 admin center", + "SubSection": "1.2 Teams & groups", + "Profile": "E3 Level 2", + "AssessmentStatus": "Automated", + "Description": "Microsoft 365 Groups is the foundational membership service that drives all teamwork across Microsoft 365. With Microsoft 365 Groups, you can give a group of people access to a collection of shared resources. While there are several different group types this recommendation concerns Microsoft 365 Groups. In the Administration panel, when a group is created, the default privacy value is 'Public'.", + "RationaleStatement": "Ensure that only organizationally managed and approved public groups exist. When a group has a 'public' privacy, users may access data related to this group (e.g. SharePoint), through three methods: By using the Azure portal, and adding themselves into the public group; By requesting access to the group from the Group application of the Access Panel; By accessing the SharePoint URL.", + "ImpactStatement": "If the recommendation is applied, group owners could receive more access requests than usual, especially regarding groups originally meant to be public.", + "RemediationProcedure": "To remediate using the UI: 1. Navigate to Microsoft 365 admin center https://admin.microsoft.com. 2. Click to expand Teams & groups select Active teams & groups. 3. On the Active teams and groups page, select the group's name that is public. 4. On the popup groups name page, Select Settings. 5. Under Privacy, select Private.", + "AuditProcedure": "To audit using the UI: 1. Navigate to Microsoft 365 admin center https://admin.microsoft.com. 2. Click to expand Teams & groups select Active teams & groups. 3. On the Active teams and groups page, check that no groups have the status 'Public' in the privacy column.", + "AdditionalInformation": "", + "References": "https://learn.microsoft.com/en-us/entra/identity/users/groups-self-service-management:https://learn.microsoft.com/en-us/microsoft-365/admin/create-groups/compare-groups?view=o365-worldwide", + "DefaultValue": "Public when created from the Administration portal; private otherwise." + } + ] + }, + { + "Id": "1.2.2", + "Description": "Shared mailboxes are used when multiple people need access to the same mailbox, such as a company information or support email address, reception desk, or other function that might be shared by multiple people. Shared mailboxes are created with a corresponding user account using a system generated password that is unknown at the time of creation. The recommended state is Sign in blocked for Shared mailboxes.", + "Checks": [], + "Attributes": [ + { + "Section": "1 Microsoft 365 admin center", + "SubSection": "1.2 Teams & groups", + "Profile": "E3 Level 1", + "AssessmentStatus": "Automated", + "Description": "Shared mailboxes are used when multiple people need access to the same mailbox, such as a company information or support email address, reception desk, or other function that might be shared by multiple people. Shared mailboxes are created with a corresponding user account using a system generated password that is unknown at the time of creation. The recommended state is Sign in blocked for Shared mailboxes.", + "RationaleStatement": "The intent of the shared mailbox is the only allow delegated access from other mailboxes. An admin could reset the password, or an attacker could potentially gain access to the shared mailbox allowing the direct sign-in to the shared mailbox and subsequently the sending of email from a sender that does not have a unique identity. To prevent this, block sign-in for the account that is associated with the shared mailbox.", + "ImpactStatement": "", + "RemediationProcedure": "To remediate using the UI: 1. Navigate to Microsoft 365 admin center https://admin.microsoft.com/ 2. Click to expand Teams & groups and select Shared mailboxes. 3. Take note of all shared mailboxes. 4. Click to expand Users and select Active users. 5. Select a shared mailbox account to open its properties pane and then select Block sign-in. 6. Check the box for Block this user from signing in. 7. Repeat for any additional shared mailboxes.", + "AuditProcedure": "To audit using the UI: 1. Navigate to Microsoft 365 admin center https://admin.microsoft.com/ 2. Click to expand Teams & groups and select Shared mailboxes. 3. Take note of all shared mailboxes. 4. Click to expand Users and select Active users. 5. Select a shared mailbox account to open its properties pane, and review. 6. Ensure the text under the name reads Sign-in blocked. 7. Repeat for any additional shared mailboxes.", + "AdditionalInformation": "", + "References": "https://learn.microsoft.com/en-us/microsoft-365/admin/email/about-shared-mailboxes?view=o365-worldwide:https://learn.microsoft.com/en-us/microsoft-365/admin/email/create-a-shared-mailbox?view=o365-worldwide#block-sign-in-for-the-shared-mailbox-account:https://learn.microsoft.com/en-us/microsoft-365/enterprise/block-user-accounts-with-microsoft-365-powershell?view=o365-worldwide#block-individual-user-accounts", + "DefaultValue": "AccountEnabled: True" + } + ] + }, + { + "Id": "1.3.1", + "Description": "Microsoft cloud-only accounts have a pre-defined password policy that cannot be changed. The only items that can change are the number of days until a password expires and whether passwords expire at all.", + "Checks": [ + "admincenter_settings_password_never_expire" + ], + "Attributes": [ + { + "Section": "1 Microsoft 365 admin center", + "SubSection": "1.3 Settings", + "Profile": "E3 Level 1", + "AssessmentStatus": "Automated", + "Description": "Microsoft cloud-only accounts have a pre-defined password policy that cannot be changed. The only items that can change are the number of days until a password expires and whether passwords expire at all.", + "RationaleStatement": "Organizations such as NIST and Microsoft have updated their password policy recommendations to not arbitrarily require users to change their passwords after a specific amount of time, unless there is evidence that the password is compromised, or the user forgot it. They suggest this even for single factor (Password Only) use cases, with a reasoning that forcing arbitrary password changes on users actually make the passwords less secure.", + "ImpactStatement": "When setting passwords not to expire it is important to have other controls in place to supplement this setting. See below for related recommendations and user guidance: Ban common passwords; Educate users to not reuse organization passwords anywhere else; Enforce Multi-Factor Authentication registration for all users.", + "RemediationProcedure": "To remediate using the UI: 1. Navigate to Microsoft 365 admin center https://admin.microsoft.com. 2. Click to expand Settings select Org Settings. 3. Click on Security & privacy. 4. Check the Set passwords to never expire (recommended) box. 5. Click Save.", + "AuditProcedure": "To audit using the UI: 1. Navigate to Microsoft 365 admin center https://admin.microsoft.com. 2. Click to expand Settings select Org Settings. 3. Click on Security & privacy. 4. Select Password expiration policy ensure that Set passwords to never expire (recommended) has been checked.", + "AdditionalInformation": "", + "References": "https://pages.nist.gov/800-63-3/sp800-63b.html:https://www.cisecurity.org/white-papers/cis-password-policy-guide/:https://learn.microsoft.com/en-us/microsoft-365/admin/misc/password-policy-recommendations?view=o365-worldwide", + "DefaultValue": "If the property is not set, a default value of 90 days will be used" + } + ] + }, + { + "Id": "1.3.2", + "Description": "Idle session timeout allows the configuration of a setting which will timeout inactive users after a pre-determined amount of time. When a user reaches the set idle timeout session, they'll get a notification that they're about to be signed out. They have to select to stay signed in or they'll be automatically signed out of all Microsoft 365 web apps. The recommended setting is 3 hours (or less) for unmanaged devices.", + "Checks": [], + "Attributes": [ + { + "Section": "1 Microsoft 365 admin center", + "SubSection": "1.3 Settings", + "Profile": "E3 Level 2", + "AssessmentStatus": "Automated", + "Description": "Idle session timeout allows the configuration of a setting which will timeout inactive users after a pre-determined amount of time. When a user reaches the set idle timeout session, they'll get a notification that they're about to be signed out. They have to select to stay signed in or they'll be automatically signed out of all Microsoft 365 web apps. The recommended setting is 3 hours (or less) for unmanaged devices.", + "RationaleStatement": "Ending idle sessions through an automatic process can help protect sensitive company data and will add another layer of security for end users who work on unmanaged devices that can potentially be accessed by the public. Unauthorized individuals onsite or remotely can take advantage of systems left unattended over time. Automatic timing out of sessions makes this more difficult.", + "ImpactStatement": "If step 2 in the Audit/Remediation procedure is left out, then there is no issue with this from a security standpoint. However, it will require users on trusted devices to sign in more frequently which could result in credential prompt fatigue.", + "RemediationProcedure": "Step 1 - Configure Idle session timeout: 1. Navigate to the Microsoft 365 admin center https://admin.microsoft.com/. 2. Click to expand Settings Select Org settings. 3. Click Security & Privacy tab. 4. Select Idle session timeout. 5. Check the box Turn on to set the period of inactivity for users to be signed off of Microsoft 365 web apps 6. Set a maximum value of 3 hours. 7. Click save. Step 2 - Ensure the Conditional Access policy is in place.", + "AuditProcedure": "Step 1 - Ensure Idle session timeout is configured: 1. Navigate to the Microsoft 365 admin center https://admin.microsoft.com/. 2. Click to expand Settings Select Org settings. 3. Click Security & Privacy tab. 4. Select Idle session timeout. 5. Verify Turn on to set the period of inactivity for users to be signed off of Microsoft 365 web apps is set to 3 hours (or less). Step 2 - Ensure the Conditional Access policy is in place.", + "AdditionalInformation": "According to Microsoft idle session timeout isn't supported when third party cookies are disabled in the browser. Users won't see any sign-out prompts.", + "References": "https://learn.microsoft.com/en-us/microsoft-365/admin/manage/idle-session-timeout-web-apps?view=o365-worldwide", + "DefaultValue": "Not configured. (Idle sessions will not timeout.)" + } + ] + }, + { + "Id": "1.3.3", + "Description": "External calendar sharing allows an administrator to enable the ability for users to share calendars with anyone outside of the organization. Outside users will be sent a URL that can be used to view the calendar.", + "Checks": [ + "admincenter_external_calendar_sharing_disabled" + ], + "Attributes": [ + { + "Section": "1 Microsoft 365 admin center", + "SubSection": "1.3 Settings", + "Profile": "E3 Level 2", + "AssessmentStatus": "Automated", + "Description": "External calendar sharing allows an administrator to enable the ability for users to share calendars with anyone outside of the organization. Outside users will be sent a URL that can be used to view the calendar.", + "RationaleStatement": "Attackers often spend time learning about organizations before launching an attack. Publicly available calendars can help attackers understand organizational relationships and determine when specific users may be more vulnerable to an attack, such as when they are traveling.", + "ImpactStatement": "This functionality is not widely used. As a result, it is unlikely that implementation of this setting will cause an impact to most users. Users that do utilize this functionality are likely to experience a minor inconvenience when scheduling meetings or synchronizing calendars with people outside the tenant.", + "RemediationProcedure": "To remediate using the UI: 1. Navigate to Microsoft 365 admin center https://admin.microsoft.com. 2. Click to expand Settings select Org settings. 3. In the Services section click Calendar. 4. Uncheck Let your users share their calendars with people outside of your organization who have Office 365 or Exchange. 5. Click Save.", + "AuditProcedure": "To audit using the UI: 1. Navigate to Microsoft 365 admin center https://admin.microsoft.com. 2. Click to expand Settings select Org settings. 3. In the Services section click Calendar. 4. Verify Let your users share their calendars with people outside of your organization who have Office 365 or Exchange is unchecked.", + "AdditionalInformation": "", + "References": "https://learn.microsoft.com/en-us/microsoft-365/admin/manage/share-calendars-with-external-users?view=o365-worldwide", + "DefaultValue": "Enabled (True)" + } + ] + }, + { + "Id": "1.3.4", + "Description": "By default, users can install add-ins in their Microsoft Word, Excel, and PowerPoint applications, allowing data access within the application. Do not allow users to install add-ins in Word, Excel, or PowerPoint.", + "Checks": [], + "Attributes": [ + { + "Section": "1 Microsoft 365 admin center", + "SubSection": "1.3 Settings", + "Profile": "E3 Level 1", + "AssessmentStatus": "Automated", + "Description": "By default, users can install add-ins in their Microsoft Word, Excel, and PowerPoint applications, allowing data access within the application. Do not allow users to install add-ins in Word, Excel, or PowerPoint.", + "RationaleStatement": "Attackers commonly use vulnerable and custom-built add-ins to access data in user applications. While allowing users to install add-ins by themselves does allow them to easily acquire useful add-ins that integrate with Microsoft applications, it can represent a risk if not used and monitored carefully. Disable future user's ability to install add-ins in Microsoft Word, Excel, or PowerPoint helps reduce your threat-surface and mitigate this risk.", + "ImpactStatement": "Implementation of this change will impact both end users and administrators. End users will not be able to install add-ins that they may want to install.", + "RemediationProcedure": "To remediate using the UI: 1. Navigate to Microsoft 365 admin center https://admin.microsoft.com. 2. Click to expand Settings > Org settings. 3. In Services select User owned apps and services. 4. Uncheck Let users access the Office Store and Let users start trials on behalf of your organization. 5. Click Save.", + "AuditProcedure": "To audit using the UI: 1. Navigate to Microsoft 365 admin center https://admin.microsoft.com. 2. Click to expand Settings > Org settings. 3. In Services select User owned apps and services. 4. Verify Let users access the Office Store and Let users start trials on behalf of your organization are not checked.", + "AdditionalInformation": "", + "References": "https://learn.microsoft.com/en-us/microsoft-365/admin/manage/manage-addins-in-the-admin-center?view=o365-worldwide#manage-add-in-downloads-by-turning-onoff-the-office-store-across-all-apps-except-outlook", + "DefaultValue": "Let users access the Office Store is Checked. Let users start trials on behalf of your organization is Checked." + } + ] + }, + { + "Id": "1.3.5", + "Description": "Microsoft Forms can be used for phishing attacks by asking personal or sensitive information and collecting the results. Microsoft 365 has built-in protection that will proactively scan for phishing attempt in forms such personal information request.", + "Checks": [], + "Attributes": [ + { + "Section": "1 Microsoft 365 admin center", + "SubSection": "1.3 Settings", + "Profile": "E3 Level 1", + "AssessmentStatus": "Automated", + "Description": "Microsoft Forms can be used for phishing attacks by asking personal or sensitive information and collecting the results. Microsoft 365 has built-in protection that will proactively scan for phishing attempt in forms such personal information request.", + "RationaleStatement": "Enabling internal phishing protection for Microsoft Forms will prevent attackers using forms for phishing attacks by asking personal or other sensitive information and URLs.", + "ImpactStatement": "If potential phishing was detected, the form will be temporarily blocked and cannot be distributed, and response collection will not happen until it is unblocked by the administrator or keywords were removed by the creator.", + "RemediationProcedure": "To remediate using the UI: 1. Navigate to Microsoft 365 admin center https://admin.microsoft.com. 2. Click to expand Settings then select Org settings. 3. Under Services select Microsoft Forms. 4. Click the checkbox labeled Add internal phishing protection under Phishing protection. 5. Click Save.", + "AuditProcedure": "To audit using the UI: 1. Navigate to Microsoft 365 admin center https://admin.microsoft.com. 2. Click to expand Settings then select Org settings. 3. Under Services select Microsoft Forms. 4. Ensure the checkbox labeled Add internal phishing protection is checked under Phishing protection.", + "AdditionalInformation": "", + "References": "https://learn.microsoft.com/en-US/microsoft-forms/administrator-settings-microsoft-forms:https://learn.microsoft.com/en-US/microsoft-forms/review-unblock-forms-users-detected-blocked-potential-phishing", + "DefaultValue": "Internal Phishing Protection is enabled." + } + ] + }, + { + "Id": "1.3.6", + "Description": "Customer Lockbox is a security feature that provides an additional layer of control and transparency to customer data in Microsoft 365. It offers an approval process for Microsoft support personnel to access organization data and creates an audited trail to meet compliance requirements.", + "Checks": [ + "admincenter_organization_customer_lockbox_enabled" + ], + "Attributes": [ + { + "Section": "1 Microsoft 365 admin center", + "SubSection": "1.3 Settings", + "Profile": "E5 Level 2", + "AssessmentStatus": "Automated", + "Description": "Customer Lockbox is a security feature that provides an additional layer of control and transparency to customer data in Microsoft 365. It offers an approval process for Microsoft support personnel to access organization data and creates an audited trail to meet compliance requirements.", + "RationaleStatement": "Enabling this feature protects organizational data against data spillage and exfiltration.", + "ImpactStatement": "Administrators will need to grant Microsoft access to the tenant environment prior to a Microsoft engineer accessing the environment for support or troubleshooting.", + "RemediationProcedure": "To remediate using the UI: 1. Navigate to Microsoft 365 admin center https://admin.microsoft.com. 2. Click to expand Settings then select Org settings. 3. Select Security & privacy tab. 4. Click Customer lockbox. 5. Check the box Require approval for all data access requests. 6. Click Save.", + "AuditProcedure": "To audit using the UI: 1. Navigate to Microsoft 365 admin center https://admin.microsoft.com. 2. Click to expand Settings then select Org settings. 3. Select Security & privacy tab. 4. Click Customer lockbox. 5. Verify Require approval for all data access requests is checked.", + "AdditionalInformation": "", + "References": "https://learn.microsoft.com/en-us/microsoft-365/compliance/customer-lockbox-requests?view=o365-worldwide", + "DefaultValue": "Disabled" + } + ] + }, + { + "Id": "1.3.7", + "Description": "Third-party storage can be enabled for users in Microsoft 365 allowing them to store and share documents using services like Dropbox, alongside OneDrive for Business and SharePoint team sites. Do not allow users to open files in third-party storage services in Microsoft 365 on the web.", + "Checks": [], + "Attributes": [ + { + "Section": "1 Microsoft 365 admin center", + "SubSection": "1.3 Settings", + "Profile": "E3 Level 2", + "AssessmentStatus": "Automated", + "Description": "Third-party storage can be enabled for users in Microsoft 365 allowing them to store and share documents using services like Dropbox, alongside OneDrive for Business and SharePoint team sites. Do not allow users to open files in third-party storage services in Microsoft 365 on the web.", + "RationaleStatement": "By using external storage services an organization may increase the risk of data breaches and data loss as the data is no longer under their direct control.", + "ImpactStatement": "Implementing this recommendation will prevent users from opening, saving, or synchronizing files with third-party storage services using Microsoft 365 on the web.", + "RemediationProcedure": "To remediate using the UI: 1. Navigate to Microsoft 365 admin center https://admin.microsoft.com. 2. Click to expand Settings then select Org settings. 3. Under Services select Microsoft 365 on the web. 4. Uncheck Let users open files stored in third-party storage services in Microsoft 365 on the web. 5. Click Save.", + "AuditProcedure": "To audit using the UI: 1. Navigate to Microsoft 365 admin center https://admin.microsoft.com. 2. Click to expand Settings then select Org settings. 3. Under Services select Microsoft 365 on the web. 4. Verify Let users open files stored in third-party storage services in Microsoft 365 on the web is not checked.", + "AdditionalInformation": "", + "References": "https://learn.microsoft.com/en-us/microsoft-365/admin/setup/set-up-file-storage-and-sharing?view=o365-worldwide", + "DefaultValue": "Enabled" + } + ] + }, + { + "Id": "1.3.8", + "Description": "Sway is a Microsoft 365 app that can be used to create newsletters, presentations, documents, and more. Ensure Sways cannot be shared with people outside of the organization.", + "Checks": [], + "Attributes": [ + { + "Section": "1 Microsoft 365 admin center", + "SubSection": "1.3 Settings", + "Profile": "E3 Level 2", + "AssessmentStatus": "Manual", + "Description": "Sway is a Microsoft 365 app that can be used to create newsletters, presentations, documents, and more. Ensure Sways cannot be shared with people outside of the organization.", + "RationaleStatement": "Disabling external sharing of Sway documents helps prevent sensitive information from being inadvertently shared outside the organization. This reduces the risk of data leakage and ensures that organizational content remains within controlled boundaries.", + "ImpactStatement": "Users will not be able to share Sway presentations with external parties, which may affect collaboration with external stakeholders, clients, or partners who need access to certain presentations.", + "RemediationProcedure": "To remediate using the UI: 1. Navigate to Microsoft 365 admin center https://admin.microsoft.com. 2. Click to expand Settings then select Org settings. 3. Under Services select Sway. 4. Uncheck Let people in your organization share their sways with people outside your organization. 5. Click Save.", + "AuditProcedure": "To audit using the UI: 1. Navigate to Microsoft 365 admin center https://admin.microsoft.com. 2. Click to expand Settings then select Org settings. 3. Under Services select Sway. 4. Verify Let people in your organization share their sways with people outside your organization is not checked.", + "AdditionalInformation": "", + "References": "https://learn.microsoft.com/en-us/microsoft-365/admin/manage/sway-admin-settings?view=o365-worldwide", + "DefaultValue": "Enabled" + } + ] + }, + { + "Id": "1.3.9", + "Description": "Microsoft Bookings is an online and mobile app for small businesses who provide services to customers on an appointment basis. Ensure shared bookings pages are restricted to select users.", + "Checks": [], + "Attributes": [ + { + "Section": "1 Microsoft 365 admin center", + "SubSection": "1.3 Settings", + "Profile": "E3 Level 1", + "AssessmentStatus": "Automated", + "Description": "Microsoft Bookings is an online and mobile app for small businesses who provide services to customers on an appointment basis. Ensure shared bookings pages are restricted to select users.", + "RationaleStatement": "Restricting the ability to create shared booking pages helps organizations maintain control over who can set up booking pages that may expose employee availability and contact information. This reduces the risk of unauthorized booking pages being created and shared, potentially exposing sensitive scheduling information.", + "ImpactStatement": "By restricting this setting, only users in selected groups will be able to create shared booking pages. This may require administrative overhead to manage group membership and respond to user requests for access.", + "RemediationProcedure": "To remediate using the UI: 1. Navigate to Microsoft 365 admin center https://admin.microsoft.com. 2. Click to expand Settings then select Org settings. 3. Under Services select Bookings. 4. Under Shared booking pages, select Allow only selected users to create Bookings. 5. Click Save.", + "AuditProcedure": "To audit using the UI: 1. Navigate to Microsoft 365 admin center https://admin.microsoft.com. 2. Click to expand Settings then select Org settings. 3. Under Services select Bookings. 4. Verify Allow only selected users to create Bookings is selected under Shared booking pages.", + "AdditionalInformation": "", + "References": "https://learn.microsoft.com/en-us/microsoft-365/bookings/turn-bookings-on-or-off?view=o365-worldwide", + "DefaultValue": "Allow your organization to use Bookings" + } + ] + }, + { + "Id": "2.1.1", + "Description": "Safe Links for Office Applications is a feature in Microsoft 365 that provides URL scanning and rewriting of inbound email messages in mail flow, and time-of-click verification of URLs and links in email messages, other Microsoft 365 applications such as Teams, and other locations such as SharePoint Online. It can help protect organizations from malicious links used in phishing and other attacks.", + "Checks": [], + "Attributes": [ + { + "Section": "2 Microsoft 365 Defender", + "SubSection": "2.1 Email & collaboration", + "Profile": "E5 Level 2", + "AssessmentStatus": "Automated", + "Description": "Safe Links for Office Applications is a feature in Microsoft 365 that provides URL scanning and rewriting of inbound email messages in mail flow, and time-of-click verification of URLs and links in email messages, other Microsoft 365 applications such as Teams, and other locations such as SharePoint Online. It can help protect organizations from malicious links used in phishing and other attacks.", + "RationaleStatement": "Safe Links for Office Applications extends phishing protection to documents and emails that contain hyperlinks, even after they have been delivered to a user.", + "ImpactStatement": "Enabling this feature may impose a slight burden on users with IT providing support for blocking issues.", + "RemediationProcedure": "To remediate using the UI: 1. Navigate to Microsoft 365 Defender https://security.microsoft.com. 2. Click to expand Email & collaboration select Policies & rules. 3. On the Policies & rules page select Threat policies. 4. Under Policies select Safe Links. 5. Click + Create or use an existing policy. 6. Enter a Policy Name and click Next. 7. Select all valid domains or groups and click Next. 8. Ensure the protection settings are enabled. 9. Click Next and finally Submit.", + "AuditProcedure": "To audit using the UI: 1. Navigate to Microsoft 365 Defender https://security.microsoft.com. 2. Click to expand Email & collaboration select Policies & rules. 3. On the Policies & rules page select Threat policies. 4. Under Policies select Safe Links. 5. Click on the policy to inspect and verify the settings.", + "AdditionalInformation": "", + "References": "https://learn.microsoft.com/en-us/defender-office-365/safe-links-policies-configure?view=o365-worldwide:https://learn.microsoft.com/en-us/powershell/module/exchange/set-safelinkspolicy?view=exchange-ps:https://learn.microsoft.com/en-us/defender-office-365/preset-security-policies?view=o365-worldwide", + "DefaultValue": "Safe Links is not enabled by default." + } + ] + }, + { + "Id": "2.1.2", + "Description": "The Common Attachment Types Filter lets a user block known and custom malicious file types from being attached to emails.", + "Checks": [ + "defender_malware_policy_common_attachments_filter_enabled" + ], + "Attributes": [ + { + "Section": "2 Microsoft 365 Defender", + "SubSection": "2.1 Email & collaboration", + "Profile": "E3 Level 1", + "AssessmentStatus": "Automated", + "Description": "The Common Attachment Types Filter lets a user block known and custom malicious file types from being attached to emails.", + "RationaleStatement": "Blocking known malicious file types can help prevent malware-infested files from infecting a host.", + "ImpactStatement": "Blocking common malicious file types should not cause an impact in modern computing environments.", + "RemediationProcedure": "To remediate using the UI: 1. Navigate to Microsoft 365 Defender https://security.microsoft.com. 2. Click to expand Email & collaboration select Policies & rules. 3. On the Policies & rules page select Threat policies. 4. Under polices select Anti-malware and click on the Default (Default) policy. 5. On the Policy page that appears on the right hand pane scroll to the bottom and click on Edit protection settings, check the Enable the common attachments filter. 6. Click Save.", + "AuditProcedure": "To audit using the UI: 1. Navigate to Microsoft 365 Defender https://security.microsoft.com. 2. Click to expand Email & collaboration select Policies & rules. 3. On the Policies & rules page select Threat policies. 4. Under Policies select Anti-malware and click on the Default (Default) policy. 5. On the policy page that appears on the righthand pane, under Protection settings, verify that the Enable the common attachments filter has the value of On.", + "AdditionalInformation": "", + "References": "https://learn.microsoft.com/en-us/powershell/module/exchange/get-malwarefilterpolicy?view=exchange-ps:https://learn.microsoft.com/en-us/defender-office-365/anti-malware-policies-configure?view=o365-worldwide", + "DefaultValue": "Always on" + } + ] + }, + { + "Id": "2.1.3", + "Description": "Exchange Online Protection (EOP) is Microsoft's cloud-based filtering service that protects organizations against spam, malware, and other email threats. EOP uses flexible anti-malware policies for malware protection settings. These policies can be set to notify Admins of malicious activity.", + "Checks": [], + "Attributes": [ + { + "Section": "2 Microsoft 365 Defender", + "SubSection": "2.1 Email & collaboration", + "Profile": "E3 Level 1", + "AssessmentStatus": "Automated", + "Description": "Exchange Online Protection (EOP) is Microsoft's cloud-based filtering service that protects organizations against spam, malware, and other email threats. EOP uses flexible anti-malware policies for malware protection settings. These policies can be set to notify Admins of malicious activity.", + "RationaleStatement": "This setting alerts administrators that an internal user sent a message that contained malware. This may indicate an account or machine compromise that would need to be investigated.", + "ImpactStatement": "Notification of account with potential issues should not have an impact on the user.", + "RemediationProcedure": "To remediate using the UI: 1. Navigate to Microsoft 365 Defender https://security.microsoft.com. 2. Click to expand E-mail & Collaboration select Policies & rules. 3. On the Policies & rules page select Threat policies. 4. Under Policies select Anti-malware. 5. Click on the Default (Default) policy. 6. Click on Edit protection settings and change the settings for Notify an admin about undelivered messages from internal senders to On and enter the email address of the administrator who should be notified under Administrator email address. 7. Click Save.", + "AuditProcedure": "To audit using the UI: 1. Navigate to Microsoft 365 Defender https://security.microsoft.com. 2. Click to expand E-mail & Collaboration select Policies & rules. 3. On the Policies & rules page select Threat policies. 4. Under Policies select Anti-malware. 5. Click on the Default (Default) policy. 6. Ensure the setting Notify an admin about undelivered messages from internal senders is set to On and that there is at least one email address under Administrator email address.", + "AdditionalInformation": "", + "References": "https://learn.microsoft.com/en-us/defender-office-365/anti-malware-protection-about:https://learn.microsoft.com/en-us/defender-office-365/anti-malware-policies-configure", + "DefaultValue": "EnableInternalSenderAdminNotifications: False, InternalSenderAdminAddress: null" + } + ] + }, + { + "Id": "2.1.4", + "Description": "The Safe Attachments policy helps protect users from malware in email attachments by scanning attachments for viruses, malware, and other malicious content. When an email attachment is received by a user, Safe Attachments will scan the attachment in a secure environment and provide a verdict on whether the attachment is safe or not.", + "Checks": [], + "Attributes": [ + { + "Section": "2 Microsoft 365 Defender", + "SubSection": "2.1 Email & collaboration", + "Profile": "E5 Level 2", + "AssessmentStatus": "Automated", + "Description": "The Safe Attachments policy helps protect users from malware in email attachments by scanning attachments for viruses, malware, and other malicious content. When an email attachment is received by a user, Safe Attachments will scan the attachment in a secure environment and provide a verdict on whether the attachment is safe or not.", + "RationaleStatement": "Enabling Safe Attachments policy helps protect against malware threats in email attachments by analyzing suspicious attachments in a secure, cloud-based environment before they are delivered to the user's inbox. This provides an additional layer of security and can prevent new or unseen types of malware from infiltrating the organization's network.", + "ImpactStatement": "Delivery of email with attachments may be delayed while scanning is occurring.", + "RemediationProcedure": "To remediate using the UI: 1. Navigate to Microsoft 365 Defender https://security.microsoft.com. 2. Click to expand E-mail & Collaboration select Policies & rules. 3. On the Policies & rules page select Threat policies. 4. Under Policies select Safe Attachments. 5. Click + Create. 6. Create a Policy Name and Description, and then click Next. 7. Select all valid domains and click Next. 8. Select Block. 9. Quarantine policy is AdminOnlyAccessPolicy. 10. Leave Enable redirect unchecked. 11. Click Next and finally Submit.", + "AuditProcedure": "To audit using the UI: 1. Navigate to Microsoft 365 Defender https://security.microsoft.com. 2. Click to expand E-mail & Collaboration select Policies & rules. 3. On the Policies & rules page select Threat policies. 4. Under Policies select Safe Attachments. 5. Inspect the highest priority policy. 6. Ensure Users and domains and Included recipient domains are in scope for the organization. 7. Ensure Safe Attachments detection response is set to Block. 8. Ensure the Quarantine Policy is set to AdminOnlyAccessPolicy. 9. Ensure the policy is not disabled.", + "AdditionalInformation": "", + "References": "https://learn.microsoft.com/en-us/defender-office-365/safe-attachments-about:https://learn.microsoft.com/en-us/defender-office-365/safe-attachments-policies-configure", + "DefaultValue": "Identity: Built-In Protection Policy, Enable: True, Action: Block, QuarantineTag: AdminOnlyAccessPolicy, Priority: lowest" + } + ] + }, + { + "Id": "2.1.5", + "Description": "Safe Attachments for SharePoint, OneDrive, and Microsoft Teams scans these services for malicious files.", + "Checks": [], + "Attributes": [ + { + "Section": "2 Microsoft 365 Defender", + "SubSection": "2.1 Email & collaboration", + "Profile": "E5 Level 2", + "AssessmentStatus": "Automated", + "Description": "Safe Attachments for SharePoint, OneDrive, and Microsoft Teams scans these services for malicious files.", + "RationaleStatement": "Safe Attachments for SharePoint, OneDrive, and Microsoft Teams protect organizations from inadvertently sharing malicious files. When a malicious file is detected that file is blocked so that no one can open, copy, move, or share it until further actions are taken by the organization's security team.", + "ImpactStatement": "Impact associated with Safe Attachments is minimal, and equivalent to impact associated with anti-virus scanners in an environment.", + "RemediationProcedure": "To remediate using the UI: 1. Navigate to Microsoft 365 Defender https://security.microsoft.com. 2. Under Email & collaboration select Policies & rules. 3. Select Threat policies then Safe Attachments. 4. Click on Global settings. 5. Click to Enable Turn on Defender for Office 365 for SharePoint, OneDrive, and Microsoft Teams. 6. Click to Enable Turn on Safe Documents for Office clients. 7. Click to Disable Allow people to click through Protected View even if Safe Documents identified the file as malicious. 8. Click Save.", + "AuditProcedure": "To audit using the UI: 1. Navigate to Microsoft 365 Defender https://security.microsoft.com. 2. Under Email & collaboration select Policies & rules. 3. Select Threat policies then Safe Attachments. 4. Click on Global settings. 5. Ensure the toggle is Enabled to Turn on Defender for Office 365 for SharePoint, OneDrive, and Microsoft Teams. 6. Ensure the toggle is Enabled to Turn on Safe Documents for Office clients. 7. Ensure the toggle is Deselected/Disabled to Allow people to click through Protected View even if Safe Documents identified the file as malicious.", + "AdditionalInformation": "", + "References": "https://learn.microsoft.com/en-us/defender-office-365/safe-attachments-for-spo-odfb-teams-about", + "DefaultValue": "EnableATPForSPOTeamsODB: False, EnableSafeDocs: False, AllowSafeDocsOpen: False" + } + ] + }, + { + "Id": "2.1.6", + "Description": "In Microsoft 365 organizations with mailboxes in Exchange Online or standalone Exchange Online Protection (EOP) organizations without Exchange Online mailboxes, email messages are automatically protected against spam (junk email) by EOP. Configure Exchange Online Spam Policies to copy emails and notify someone when a sender in the organization has been blocked for sending spam emails.", + "Checks": [], + "Attributes": [ + { + "Section": "2 Microsoft 365 Defender", + "SubSection": "2.1 Email & collaboration", + "Profile": "E3 Level 1", + "AssessmentStatus": "Automated", + "Description": "In Microsoft 365 organizations with mailboxes in Exchange Online or standalone Exchange Online Protection (EOP) organizations without Exchange Online mailboxes, email messages are automatically protected against spam (junk email) by EOP. Configure Exchange Online Spam Policies to copy emails and notify someone when a sender in the organization has been blocked for sending spam emails.", + "RationaleStatement": "A blocked account is a good indication that the account in question has been breached, and an attacker is using it to send spam emails to other people.", + "ImpactStatement": "Notification of users that have been blocked should not cause an impact to the user.", + "RemediationProcedure": "To remediate using the UI: 1. Navigate to Microsoft 365 Defender https://security.microsoft.com. 2. Click to expand Email & collaboration select Policies & rules > Threat policies. 3. Under Policies select Anti-spam. 4. Click on the Anti-spam outbound policy (default). 5. Select Edit protection settings then under Notifications. 6. Check Send a copy of suspicious outbound messages or message that exceed these limits to these users and groups then enter the desired email addresses. 7. Check Notify these users and groups if a sender is blocked due to sending outbound spam then enter the desired email addresses. 8. Click Save.", + "AuditProcedure": "To audit using the UI: 1. Navigate to Microsoft 365 Defender https://security.microsoft.com. 2. Click to expand Email & collaboration select Policies & rules > Threat policies. 3. Under Policies select Anti-spam. 4. Click on the Anti-spam outbound policy (default). 5. Verify that Send a copy of suspicious outbound messages or message that exceed these limits to these users and groups is set to On, ensure the email address is correct. 6. Verify that Notify these users and groups if a sender is blocked due to sending outbound spam is set to On, ensure the email address is correct.", + "AdditionalInformation": "", + "References": "https://learn.microsoft.com/en-us/defender-office-365/outbound-spam-protection-about", + "DefaultValue": "BccSuspiciousOutboundAdditionalRecipients: {}, BccSuspiciousOutboundMail: False, NotifyOutboundSpamRecipients: {}, NotifyOutboundSpam: False" + } + ] + }, + { + "Id": "2.1.7", + "Description": "By default, Office 365 includes built-in features that help protect users from phishing attacks. Set up anti-phishing polices to increase this protection, for example by refining settings to better detect and prevent impersonation and spoofing attacks. The default policy applies to all users within the organization and is a single view to fine-tune anti-phishing protection. Custom policies can be created and configured for specific users, groups or domains within the organization and will take precedence over the default policy for the scoped users.", + "Checks": [ + "defender_antiphishing_policy_configured" + ], + "Attributes": [ + { + "Section": "2 Microsoft 365 Defender", + "SubSection": "2.1 Email & collaboration", + "Profile": "E5 Level 2", + "AssessmentStatus": "Automated", + "Description": "By default, Office 365 includes built-in features that help protect users from phishing attacks. Set up anti-phishing polices to increase this protection, for example by refining settings to better detect and prevent impersonation and spoofing attacks. The default policy applies to all users within the organization and is a single view to fine-tune anti-phishing protection. Custom policies can be created and configured for specific users, groups or domains within the organization and will take precedence over the default policy for the scoped users.", + "RationaleStatement": "Protects users from phishing attacks (like impersonation and spoofing) and uses safety tips to warn users about potentially harmful messages.", + "ImpactStatement": "Mailboxes that are used for support systems such as helpdesk and billing systems send mail to internal users and are often not suitable candidates for impersonation protection. Care should be taken to ensure that these systems are excluded from Impersonation Protection.", + "RemediationProcedure": "To remediate using the UI: 1. Navigate to Microsoft 365 Defender https://security.microsoft.com. 2. Click to expand Email & collaboration select Policies & rules. 3. Select Threat policies. 4. Under Policies select Anti-phishing and click Create. 5. Name the policy and configure Users, groups, and domains to include a majority of the organization. 6. Set Phishing email threshold to 3 - More Aggressive. 7. Check Enable users to protect and add up to 350 users. 8. Check Enable domains to protect and check Include domains I own. 9. Enable mailbox intelligence, spoof intelligence, and impersonation protection. 10. Under Actions set all detection responses to Quarantine the message. 11. Enable all safety tips. 12. Click Next and finally Submit.", + "AuditProcedure": "To audit using the UI: 1. Navigate to Microsoft 365 Defender https://security.microsoft.com. 2. Click to expand Email & collaboration select Policies & rules. 3. Select Threat policies. 4. Under Policies select Anti-phishing. 5. Ensure an AntiPhish policy exists that is On and meets the criteria: Phishing email threshold at least 3, User and domain impersonation protection enabled, Mailbox intelligence enabled, Actions set to Quarantine the message, Safety tips enabled, Honor DMARC record policy enabled.", + "AdditionalInformation": "", + "References": "https://learn.microsoft.com/en-us/defender-office-365/anti-phishing-protection-about:https://learn.microsoft.com/en-us/defender-office-365/anti-phishing-policies-eop-configure", + "DefaultValue": "Anti-phishing policies exist by default but may not have all recommended settings enabled." + } + ] + }, + { + "Id": "2.1.8", + "Description": "For each domain that is configured in Exchange, a corresponding Sender Policy Framework (SPF) record should be created.", + "Checks": [], + "Attributes": [ + { + "Section": "2 Microsoft 365 Defender", + "SubSection": "2.1 Email & collaboration", + "Profile": "E3 Level 1", + "AssessmentStatus": "Automated", + "Description": "For each domain that is configured in Exchange, a corresponding Sender Policy Framework (SPF) record should be created.", + "RationaleStatement": "SPF records allow Exchange Online Protection and other mail systems to know where messages from domains are allowed to originate. This information can be used by that system to determine how to treat the message based on if it is being spoofed or is valid.", + "ImpactStatement": "There should be minimal impact of setting up SPF records however, organizations should ensure proper SPF record setup as email could be flagged as spam if SPF is not setup appropriately.", + "RemediationProcedure": "To remediate using a DNS Provider: 1. If all email in your domain is sent from and received by Exchange Online, add the following TXT record for each Accepted Domain: v=spf1 include:spf.protection.outlook.com -all. 2. If there are other systems that send email in the environment, refer to Microsoft documentation for the proper SPF configuration.", + "AuditProcedure": "To audit using PowerShell: 1. Open a command prompt. 2. Type the following command in PowerShell: Resolve-DnsName [domain1.com] txt | fl. 3. Ensure that a value exists and that it includes v=spf1 include:spf.protection.outlook.com. This designates Exchange Online as a designated sender.", + "AdditionalInformation": "Resolve-DnsName is not available on older versions of Windows prior to Windows 8 and Server 2012.", + "References": "https://learn.microsoft.com/en-us/microsoft-365/security/office-365-security/email-authentication-spf-configure?view=o365-worldwide", + "DefaultValue": "SPF records are not configured by default." + } + ] + }, + { + "Id": "2.1.9", + "Description": "DKIM is one of the trio of Authentication methods (SPF, DKIM and DMARC) that help prevent attackers from sending messages that look like they come from your domain. DKIM lets an organization add a digital signature to outbound email messages in the message header. When DKIM is configured, the organization authorizes its domain to associate, or sign, its name to an email message using cryptographic authentication.", + "Checks": [ + "defender_domain_dkim_enabled" + ], + "Attributes": [ + { + "Section": "2 Microsoft 365 Defender", + "SubSection": "2.1 Email & collaboration", + "Profile": "E3 Level 1", + "AssessmentStatus": "Automated", + "Description": "DKIM is one of the trio of Authentication methods (SPF, DKIM and DMARC) that help prevent attackers from sending messages that look like they come from your domain. DKIM lets an organization add a digital signature to outbound email messages in the message header. When DKIM is configured, the organization authorizes its domain to associate, or sign, its name to an email message using cryptographic authentication.", + "RationaleStatement": "By enabling DKIM with Office 365, messages that are sent from Exchange Online will be cryptographically signed. This will allow the receiving email system to validate that the messages were generated by a server that the organization authorized and not being spoofed.", + "ImpactStatement": "There should be no impact of setting up DKIM however, organizations should ensure appropriate setup to ensure continuous mail-flow.", + "RemediationProcedure": "To remediate using a DNS Provider: 1. For each accepted domain in Exchange Online, two DNS entries are required. 2. After the DNS records are created, enable DKIM signing in Defender. 3. Navigate to Microsoft 365 Defender https://security.microsoft.com/. 4. Expand Email & collaboration > Policies & rules > Threat policies. 5. Under Rules section click Email authentication settings. 6. Select DKIM. 7. Click on each domain and click Enable next to Sign messages for this domain with DKIM signature.", + "AuditProcedure": "To audit using the UI: 1. Navigate to Microsoft 365 Defender https://security.microsoft.com/. 2. Expand Email & collaboration > Policies & rules > Threat policies. 3. Under Rules section click Email authentication settings. 4. Select DKIM. 5. Click on each domain and confirm that Sign messages for this domain with DKIM signatures is Enabled and Status reads Signing DKIM signatures for this domain.", + "AdditionalInformation": "", + "References": "https://learn.microsoft.com/en-us/microsoft-365/security/office-365-security/email-authentication-dkim-configure?view=o365-worldwide", + "DefaultValue": "DKIM is not enabled by default." + } + ] + }, + { + "Id": "2.1.10", + "Description": "DMARC, or Domain-based Message Authentication, Reporting, and Conformance, assists recipient mail systems in determining the appropriate action to take when messages from a domain fail to meet SPF or DKIM authentication criteria.", + "Checks": [], + "Attributes": [ + { + "Section": "2 Microsoft 365 Defender", + "SubSection": "2.1 Email & collaboration", + "Profile": "E3 Level 1", + "AssessmentStatus": "Automated", + "Description": "DMARC, or Domain-based Message Authentication, Reporting, and Conformance, assists recipient mail systems in determining the appropriate action to take when messages from a domain fail to meet SPF or DKIM authentication criteria.", + "RationaleStatement": "DMARC strengthens the trustworthiness of messages sent from an organization's domain to destination email systems. By integrating DMARC with SPF (Sender Policy Framework) and DKIM (DomainKeys Identified Mail), organizations can significantly enhance their defenses against email spoofing and phishing attempts. Leaving a DMARC policy set to p=none can result in failed action when a spear phishing email fails DMARC but passes SPF and DKIM checks. Having DMARC fully configured is a critical part in preventing business email compromise.", + "ImpactStatement": "There should be no impact of setting up DMARC however, organizations should ensure appropriate setup to ensure continuous mail-flow.", + "RemediationProcedure": "To remediate using a DNS Provider: 1. For each Exchange Online Accepted Domain, add the following record to DNS: Record: _dmarc.domain1.com, Type: TXT, Value: v=DMARC1; p=none; rua=mailto:; ruf=mailto:. 2. This will create a basic DMARC policy. 3. After monitoring, implement a policy of p=reject OR p=quarantine and pct=100.", + "AuditProcedure": "To audit using PowerShell: 1. Open a command prompt. 2. For each of the Accepted Domains in Exchange Online run the following in PowerShell: Resolve-DnsName _dmarc.[domain1.com] txt. 3. Ensure that the record exists and has at minimum the following flags defined: v=DMARC1; (p=quarantine OR p=reject), pct=100, rua=mailto: and ruf=mailto:. 4. Ensure the Microsoft MOERA domain is also configured.", + "AdditionalInformation": "The remediation portion involves a multi-staged approach over a period of time.", + "References": "https://learn.microsoft.com/en-us/defender-office-365/email-authentication-dmarc-configure?view=o365-worldwide:https://learn.microsoft.com/en-us/defender-office-365/step-by-step-guides/how-to-enable-dmarc-reporting-for-microsoft-online-email-routing-address-moera-and-parked-domains?view=o365-worldwide", + "DefaultValue": "DMARC records are not configured by default." + } + ] + }, + { + "Id": "2.1.11", + "Description": "The Common Attachment Types Filter lets a user block known and custom malicious file types from being attached to emails. The policy provided by Microsoft covers 53 extensions, and an additional custom list of extensions can be defined. The list of 184 extensions provided in this recommendation is comprehensive but not exhaustive.", + "Checks": [ + "defender_malware_policy_comprehensive_attachments_filter_applied" + ], + "Attributes": [ + { + "Section": "2 Microsoft 365 Defender", + "SubSection": "2.1 Email & collaboration", + "Profile": "E3 Level 2", + "AssessmentStatus": "Automated", + "Description": "The Common Attachment Types Filter lets a user block known and custom malicious file types from being attached to emails. The policy provided by Microsoft covers 53 extensions, and an additional custom list of extensions can be defined. The list of 184 extensions provided in this recommendation is comprehensive but not exhaustive.", + "RationaleStatement": "Blocking known malicious file types can help prevent malware-infested files from infecting a host or performing other malicious attacks such as phishing and data extraction. Defining a comprehensive list of attachments can help protect against additional unknown and known threats. Many legacy file formats, binary files and compressed files have been used as delivery mechanisms for malicious software.", + "ImpactStatement": "For file types that are business necessary users will need to use other organizationally approved methods to transfer blocked extension types between business partners.", + "RemediationProcedure": "To Remediate using PowerShell: 1. Connect to Exchange Online using Connect-ExchangeOnline. 2. Run the script provided to create an attachment policy and associated rule with 184 extensions. 3. When prepared enable the rule either through the UI or PowerShell.", + "AuditProcedure": "For this control, a Level 2 comprehensive attachment policy is defined as one that includes at least 120 extensions. To pass, organizations must demonstrate at least a 90% adoption rate of the extension list referenced in the audit script, with allowances for justified exceptions.", + "AdditionalInformation": "Organizations should evaluate any extensions missing from the report to determine if they are valid exceptions.", + "References": "https://learn.microsoft.com/en-us/powershell/module/exchange/get-malwarefilterpolicy?view=exchange-ps:https://learn.microsoft.com/en-us/microsoft-365/security/office-365-security/anti-malware-policies-configure?view=o365-worldwide:https://learn.microsoft.com/en-us/office/compatibility/office-file-format-reference", + "DefaultValue": "53 extensions are blocked by default." + } + ] + }, + { + "Id": "2.1.12", + "Description": "In Microsoft 365 organizations with Exchange Online mailboxes or standalone Exchange Online Protection (EOP) organizations without Exchange Online mailboxes, connection filtering and the default connection filter policy identify good or bad source email servers by IP addresses. The key components of the default connection filter policy are IP Allow List, IP Block List and Safe list. The recommended state is IP Allow List empty or undefined.", + "Checks": [ + "defender_antispam_connection_filter_policy_empty_ip_allowlist" + ], + "Attributes": [ + { + "Section": "2 Microsoft 365 Defender", + "SubSection": "2.1 Email & collaboration", + "Profile": "E3 Level 1", + "AssessmentStatus": "Automated", + "Description": "In Microsoft 365 organizations with Exchange Online mailboxes or standalone Exchange Online Protection (EOP) organizations without Exchange Online mailboxes, connection filtering and the default connection filter policy identify good or bad source email servers by IP addresses. The key components of the default connection filter policy are IP Allow List, IP Block List and Safe list. The recommended state is IP Allow List empty or undefined.", + "RationaleStatement": "Without additional verification like mail flow rules, email from sources in the IP Allow List skips spam filtering and sender authentication (SPF, DKIM, DMARC) checks. This method creates a high risk of attackers successfully delivering email to the Inbox that would otherwise be filtered. Messages that are determined to be malware or high confidence phishing are filtered.", + "ImpactStatement": "This is the default behavior. IP Allow lists may reduce false positives, however, this benefit is outweighed by the importance of a policy which scans all messages regardless of the origin. This supports the principle of zero trust.", + "RemediationProcedure": "To remediate using the UI: 1. Navigate to Microsoft 365 Defender https://security.microsoft.com. 2. Click to expand Email & collaboration select Policies & rules > Threat policies. 3. Under Policies select Anti-spam. 4. Click on the Connection filter policy (Default). 5. Click Edit connection filter policy. 6. Remove any IP entries from Always allow messages from the following IP addresses or address range. 7. Click Save.", + "AuditProcedure": "To audit using the UI: 1. Navigate to Microsoft 365 Defender https://security.microsoft.com. 2. Click to expand Email & collaboration select Policies & rules > Threat policies. 3. Under Policies select Anti-spam. 4. Click on the Connection filter policy (Default). 5. Ensure IP Allow list contains no entries.", + "AdditionalInformation": "", + "References": "https://learn.microsoft.com/en-us/defender-office-365/connection-filter-policies-configure:https://learn.microsoft.com/en-us/defender-office-365/create-safe-sender-lists-in-office-365#use-the-ip-allow-list:https://learn.microsoft.com/en-us/defender-office-365/how-policies-and-protections-are-combined#user-and-tenant-settings-conflict", + "DefaultValue": "IPAllowList: {}" + } + ] + }, + { + "Id": "2.1.13", + "Description": "In Microsoft 365 organizations with Exchange Online mailboxes or standalone Exchange Online Protection (EOP) organizations without Exchange Online mailboxes, connection filtering and the default connection filter policy identify good or bad source email servers by IP addresses. The key components of the default connection filter policy are IP Allow List, IP Block List and Safe list. The safe list is a pre-configured allow list that is dynamically updated by Microsoft. The recommended safe list state is: Off or False.", + "Checks": [ + "defender_antispam_connection_filter_policy_safe_list_off" + ], + "Attributes": [ + { + "Section": "2 Microsoft 365 Defender", + "SubSection": "2.1 Email & collaboration", + "Profile": "E3 Level 1", + "AssessmentStatus": "Automated", + "Description": "In Microsoft 365 organizations with Exchange Online mailboxes or standalone Exchange Online Protection (EOP) organizations without Exchange Online mailboxes, connection filtering and the default connection filter policy identify good or bad source email servers by IP addresses. The key components of the default connection filter policy are IP Allow List, IP Block List and Safe list. The safe list is a pre-configured allow list that is dynamically updated by Microsoft. The recommended safe list state is: Off or False.", + "RationaleStatement": "Without additional verification like mail flow rules, email from sources in the IP Allow List skips spam filtering and sender authentication (SPF, DKIM, DMARC) checks. This method creates a high risk of attackers successfully delivering email to the Inbox that would otherwise be filtered. The safe list is managed dynamically by Microsoft, and administrators do not have visibility into which senders are included.", + "ImpactStatement": "This is the default behavior. IP Allow lists may reduce false positives, however, this benefit is outweighed by the importance of a policy which scans all messages regardless of the origin. This supports the principle of zero trust.", + "RemediationProcedure": "To remediate using the UI: 1. Navigate to Microsoft 365 Defender https://security.microsoft.com. 2. Click to expand Email & collaboration select Policies & rules > Threat policies. 3. Under Policies select Anti-spam. 4. Click on the Connection filter policy (Default). 5. Click Edit connection filter policy. 6. Uncheck Turn on safe list. 7. Click Save.", + "AuditProcedure": "To audit using the UI: 1. Navigate to Microsoft 365 Defender https://security.microsoft.com. 2. Click to expand Email & collaboration select Policies & rules > Threat policies. 3. Under Policies select Anti-spam. 4. Click on the Connection filter policy (Default). 5. Ensure Safe list is Off.", + "AdditionalInformation": "", + "References": "https://learn.microsoft.com/en-us/defender-office-365/connection-filter-policies-configure:https://learn.microsoft.com/en-us/defender-office-365/create-safe-sender-lists-in-office-365#use-the-ip-allow-list:https://learn.microsoft.com/en-us/defender-office-365/how-policies-and-protections-are-combined#user-and-tenant-settings-conflict", + "DefaultValue": "EnableSafeList: False" + } + ] + }, + { + "Id": "2.1.14", + "Description": "Anti-spam protection is a feature of Exchange Online that utilizes policies to help to reduce the amount of junk email, bulk and phishing emails a mailbox receives. These policies contain lists to allow or block specific senders or domains. The recommended state is: Do not define any Allowed domains.", + "Checks": [ + "defender_antispam_policy_inbound_no_allowed_domains" + ], + "Attributes": [ + { + "Section": "2 Microsoft 365 Defender", + "SubSection": "2.1 Email & collaboration", + "Profile": "E3 Level 1", + "AssessmentStatus": "Automated", + "Description": "Anti-spam protection is a feature of Exchange Online that utilizes policies to help to reduce the amount of junk email, bulk and phishing emails a mailbox receives. These policies contain lists to allow or block specific senders or domains. The recommended state is: Do not define any Allowed domains.", + "RationaleStatement": "Messages from entries in the allowed senders list or the allowed domains list bypass most email protection (except malware and high confidence phishing) and email authentication checks (SPF, DKIM and DMARC). Entries in the allowed senders list or the allowed domains list create a high risk of attackers successfully delivering email to the Inbox that would otherwise be filtered. The risk is increased even more when allowing common domain names as these can be easily spoofed by attackers.", + "ImpactStatement": "This is the default behavior. Allowed domains may reduce false positives, however, this benefit is outweighed by the importance of having a policy which scans all messages regardless of the origin. This supports the principle of zero trust.", + "RemediationProcedure": "To remediate using the UI: 1. Navigate to Microsoft 365 Defender https://security.microsoft.com. 2. Click to expand Email & collaboration select Policies & rules > Threat policies. 3. Under Policies select Anti-spam. 4. Open each out of compliance inbound anti-spam policy by clicking on it. 5. Click Edit allowed and blocked senders and domains. 6. Select Allow domains. 7. Delete each domain from the domains list. 8. Click Done > Save. 9. Repeat as needed.", + "AuditProcedure": "To audit using the UI: 1. Navigate to Microsoft 365 Defender https://security.microsoft.com. 2. Click to expand Email & collaboration select Policies & rules > Threat policies. 3. Under Policies select Anti-spam. 4. Inspect each inbound anti-spam policy. 5. Ensure that Allowed domains does not contain any domain names. 6. Repeat as needed for any additional inbound anti-spam policy.", + "AdditionalInformation": "Microsoft specifies in its documentation that allowed domains should be used for testing purposes only.", + "References": "https://learn.microsoft.com/en-us/defender-office-365/anti-spam-protection-about#allow-and-block-lists-in-anti-spam-policies", + "DefaultValue": "AllowedSenderDomains: {}" + } + ] + }, + { + "Id": "2.1.15", + "Description": "The default outbound anti-spam policy in Microsoft Defender automatically applies to all users and is designed to detect and limit suspicious email-sending behavior. The policy enforces limits based on both volume and spam detection. The recommended state is: External: 500, Internal: 1000, Daily: 1000, Action: Restrict the user from sending mail.", + "Checks": [ + "defender_antispam_outbound_policy_configured", + "defender_antispam_outbound_policy_forwarding_disabled" + ], + "Attributes": [ + { + "Section": "2 Microsoft 365 Defender", + "SubSection": "2.1 Email & collaboration", + "Profile": "E3 Level 1", + "AssessmentStatus": "Automated", + "Description": "The default outbound anti-spam policy in Microsoft Defender automatically applies to all users and is designed to detect and limit suspicious email-sending behavior. The policy enforces limits based on both volume and spam detection. The recommended state is: External: 500, Internal: 1000, Daily: 1000, Action: Restrict the user from sending mail.", + "RationaleStatement": "Message limit settings help lessen the impact of a Business Email Compromise (BEC) by automatically restricting accounts that send unusually high volumes of email. This containment prevents compromised accounts from launching large-scale attacks and helps ensure the organization's email remains trusted and deliverable.", + "ImpactStatement": "Enforcing message limits may result in legitimate users being temporarily blocked from sending email if their bulk messaging activity resembles spam or exceeds volume thresholds. This can disrupt business operations, delay communication, and require administrative effort to investigate and restore access.", + "RemediationProcedure": "To remediate using the UI: 1. Navigate to Microsoft 365 Defender https://security.microsoft.com. 2. Click to expand Email & collaboration select Policies & rules > Threat policies. 3. Under Policies select Anti-spam and click to open Anti-spam outbound policy (Default). 4. Select Edit protection settings. 5. Set External: 500, Internal: 1000, Daily: 1000, Action: Restrict the user from sending mail. 6. Ensure Notify these users and groups if a sender is blocked contains a monitored mailbox.", + "AuditProcedure": "To audit using the UI: 1. Navigate to Microsoft 365 Defender https://security.microsoft.com. 2. Click to expand Email & collaboration select Policies & rules > Threat policies. 3. Under Policies select Anti-spam and click to open Anti-spam outbound policy (Default). 4. Ensure the following settings are to the recommended level or more restrictive: External: 500, Internal: 1000, Daily: 1000, Action: Restrict the user from sending mail. 5. Ensure a monitored mailbox is configured.", + "AdditionalInformation": "Microsoft's Recommended Strict values represent a more restrictive and also compliant configuration: 400, 800, and 800.", + "References": "https://learn.microsoft.com/en-us/defender-office-365/outbound-spam-protection-about:https://learn.microsoft.com/en-us/defender-office-365/recommended-settings-for-eop-and-office365#outbound-spam-policy-settings:https://learn.microsoft.com/en-us/office365/servicedescriptions/exchange-online-service-description/exchange-online-limits#sending-limits-1", + "DefaultValue": "RecipientLimitExternalPerHour: 0, RecipientLimitInternalPerHour: 0, RecipientLimitPerDay: 0, ActionWhenThresholdReached: BlockUserForToday" + } + ] + }, + { + "Id": "2.2.1", + "Description": "Organizations should monitor sign-in and audit log activity from the emergency accounts and trigger notifications to other administrators. When you monitor the activity for emergency access accounts, you can verify these accounts are only used for testing or actual emergencies. This recommendation uses Defender for Cloud Apps Policies to alert on emergency access account activity. The recommended state is to monitor Activity type Log on on break-glass or emergency access accounts.", + "Checks": [], + "Attributes": [ + { + "Section": "2 Microsoft 365 Defender", + "SubSection": "2.2 Cloud apps", + "Profile": "E5 Level 1", + "AssessmentStatus": "Manual", + "Description": "Organizations should monitor sign-in and audit log activity from the emergency accounts and trigger notifications to other administrators. When you monitor the activity for emergency access accounts, you can verify these accounts are only used for testing or actual emergencies. This recommendation uses Defender for Cloud Apps Policies to alert on emergency access account activity. The recommended state is to monitor Activity type Log on on break-glass or emergency access accounts.", + "RationaleStatement": "Emergency access accounts should be used in very few scenarios, for example, the last Global Administrator has left the organization and the account is inaccessible. All activity on an emergency access account should be reviewed at the time of the event to ensure the sign on is legitimate and authorized.", + "ImpactStatement": "There is no real world impact to monitoring these accounts beyond allocating staff. The frequency of emergency account sign on should be so low that any activity raises a red flag that is treated with the highest priority.", + "RemediationProcedure": "To remediate using the UI: 1. Navigate to Microsoft 365 Defender https://security.microsoft.com. 2. Under the Cloud Apps section select Policies -> Policy management. 3. Click on All policies and then Create policy -> Activity policy. 4. Give the policy a name and set: Policy severity to High severity, Category to Privileged accounts, Act on Single activity. 5. Click Select a filter -> Activity type equals Log on. 6. Click Add a filter -> User Name equals as Any role. 7. Ensure all emergency access accounts are added. 8. Select an alert method such as Send alert as email.", + "AuditProcedure": "To audit using the UI: 1. Navigate to Microsoft 365 Defender https://security.microsoft.com. 2. Under the Cloud Apps section select Policies -> Policy management. 3. Locate a privileged accounts policy that meets the criteria: Policy severity is High severity, Category is Privileged accounts, Act on Single activity is selected, Filter includes Activity type equals Log on and User Name equals emergency access accounts, Alerting is configured. 4. Repeat for any additional emergency access accounts.", + "AdditionalInformation": "Multiple accounts can be monitored by a single policy or by separate policies. Emergency access account activity can be monitored in various ways.", + "References": "https://learn.microsoft.com/en-us/entra/identity/role-based-access-control/security-emergency-access#monitor-sign-in-and-audit-logs:https://learn.microsoft.com/en-us/defender-cloud-apps/control-cloud-apps-with-policies", + "DefaultValue": "A policy to monitor emergency access accounts does not exist by default." + } + ] + }, + { + "Id": "2.4.1", + "Description": "Identify priority accounts to utilize Microsoft 365's advanced custom security features. This is an essential tool to bolster protection for users who are frequently targeted due to their critical positions, such as executives, leaders, managers, or others who have access to sensitive, confidential, financial, or high-priority information.", + "Checks": [], + "Attributes": [ + { + "Section": "2 Microsoft 365 Defender", + "SubSection": "2.4 System", + "Profile": "E5 Level 1", + "AssessmentStatus": "Automated", + "Description": "Identify priority accounts to utilize Microsoft 365's advanced custom security features. This is an essential tool to bolster protection for users who are frequently targeted due to their critical positions, such as executives, leaders, managers, or others who have access to sensitive, confidential, financial, or high-priority information.", + "RationaleStatement": "Enabling priority account protection for users in Microsoft 365 is necessary to enhance security for accounts with access to sensitive data and high privileges, such as CEOs, CISOs, CFOs, and IT admins. These priority accounts are often targeted by spear phishing or whaling attacks and require stronger protection to prevent account compromise.", + "ImpactStatement": "No significant negative impact. Priority account protection enhances security monitoring and alerting for designated accounts.", + "RemediationProcedure": "To remediate using the UI: Step 1: Enable Priority account protection: 1. Navigate to Microsoft 365 Defender https://security.microsoft.com/. 2. Click to expand System select Settings. 3. Select E-mail & Collaboration > Priority account protection. 4. Ensure Priority account protection is set to On. Step 2: Tag priority accounts: Select User tags, add members to PRIORITY ACCOUNT tag. Step 3: Configure E-mail alerts for Priority Accounts: Create alert policies for Detected malware and Phishing email detected activities targeting Priority accounts.", + "AuditProcedure": "To audit using the UI: Step 1: Verify Priority account protection is enabled in Settings > E-mail & collaboration > Priority account protection. Step 2: Verify priority accounts are identified and tagged. Step 3: Ensure alert policies are configured for malware and phishing detection targeting priority accounts with High severity.", + "AdditionalInformation": "", + "References": "https://learn.microsoft.com/en-us/microsoft-365/admin/setup/priority-accounts:https://learn.microsoft.com/en-us/defender-office-365/priority-accounts-security-recommendations", + "DefaultValue": "By default, priority accounts are undefined." + } + ] + }, + { + "Id": "2.4.2", + "Description": "Preset security policies have been established by Microsoft, utilizing observations and experiences within datacenters to strike a balance between the exclusion of malicious content from users and limiting unwarranted disruptions. Strict protection has the most aggressive protection of the 3 presets.", + "Checks": [], + "Attributes": [ + { + "Section": "2 Microsoft 365 Defender", + "SubSection": "2.4 System", + "Profile": "E5 Level 1", + "AssessmentStatus": "Automated", + "Description": "Preset security policies have been established by Microsoft, utilizing observations and experiences within datacenters to strike a balance between the exclusion of malicious content from users and limiting unwarranted disruptions. Strict protection has the most aggressive protection of the 3 presets.", + "RationaleStatement": "Enabling priority account protection for users in Microsoft 365 is necessary to enhance security for accounts with access to sensitive data and high privileges. The implementation of stringent, pre-defined policies may result in instances of false positive, however, the benefit of requiring the end-user to preview junk email before accessing their inbox outweighs the potential risk of mistakenly perceiving a malicious email as safe.", + "ImpactStatement": "Strict policies are more likely to cause false positives in anti-spam, phishing, impersonation, spoofing and intelligence responses.", + "RemediationProcedure": "To remediate using the UI: 1. Navigate to Microsoft 365 Defender https://security.microsoft.com/. 2. Select to expand E-mail & collaboration. 3. Select Policies & rules > Threat policies > Preset security policies. 4. Click to Manage protection settings for Strict protection preset. 5. For Apply Exchange Online Protection select at minimum Specific recipients and include the Accounts/Groups identified as Priority Accounts. 6. For Apply Defender for Office 365 Protection select at minimum Specific recipients. 7. For Impersonation protection add valid e-mails. 8. For Protected custom domains add the organization's domain name. 9. Click Next and finally Confirm.", + "AuditProcedure": "To audit using the UI: 1. Navigate to Microsoft 365 Defender https://security.microsoft.com/. 2. Select to expand E-mail & collaboration. 3. Select Policies & rules > Threat policies. 4. From here visit each section: Anti-phishing, Anti-spam, Anti-malware, Safe Attachments, Safe Links. 5. Ensure in each there is a policy named Strict Preset Security Policy which includes the organization's priority Accounts/Groups.", + "AdditionalInformation": "The preset security polices cannot target Priority account TAGS currently, groups should be used instead.", + "References": "https://learn.microsoft.com/en-us/defender-office-365/preset-security-policies?view=o365-worldwide:https://learn.microsoft.com/en-us/defender-office-365/priority-accounts-security-recommendations:https://learn.microsoft.com/en-us/defender-office-365/recommended-settings-for-eop-and-office365?view=o365-worldwide", + "DefaultValue": "By default, presets are not applied to any users or groups." + } + ] + }, + { + "Id": "2.4.3", + "Description": "Microsoft Defender for Cloud Apps is a Cloud Access Security Broker (CASB). It provides visibility into suspicious activity in Microsoft 365, enabling investigation into potential security issues and facilitating the implementation of remediation measures if necessary.", + "Checks": [], + "Attributes": [ + { + "Section": "2 Microsoft 365 Defender", + "SubSection": "2.4 System", + "Profile": "E5 Level 2", + "AssessmentStatus": "Manual", + "Description": "Microsoft Defender for Cloud Apps is a Cloud Access Security Broker (CASB). It provides visibility into suspicious activity in Microsoft 365, enabling investigation into potential security issues and facilitating the implementation of remediation measures if necessary.", + "RationaleStatement": "Security teams can receive notifications of triggered alerts for atypical or suspicious activities, see how the organization's data in Microsoft 365 is accessed and used, suspend user accounts exhibiting suspicious activity, and require users to log back in to Microsoft 365 apps after an alert has been triggered.", + "ImpactStatement": "Additional configuration and monitoring overhead. May require additional licensing for full functionality.", + "RemediationProcedure": "To remediate using the UI: 1. Navigate to Microsoft 365 Defender https://security.microsoft.com/. 2. Click to expand System select Settings > Cloud apps. 3. Scroll to Information Protection and select Files. 4. Check Enable file monitoring. 5. Scroll up to Cloud Discovery and select Microsoft Defender for Endpoint. 6. Check Enforce app access, configure a Notification URL and Save. Configure App Connectors: Scroll to Connected apps and select App connectors, connect Microsoft 365 and Microsoft Azure applications.", + "AuditProcedure": "To audit using the UI: 1. Navigate to Microsoft 365 Defender https://security.microsoft.com/. 2. Click to expand System select Settings > Cloud apps. 3. Scroll to Connected apps and select App connectors. 4. Ensure that Microsoft 365 and Microsoft Azure both show in the list as Connected. 5. Go to Cloud Discovery > Microsoft Defender for Endpoint and check if the integration is enabled. 6. Go to Information Protection > Files and verify Enable file monitoring is checked.", + "AdditionalInformation": "Defender for Endpoint requires a Defender for Endpoint license. Some risk detection methods provided by Entra Identity Protection also require Microsoft Defender for Cloud Apps.", + "References": "https://learn.microsoft.com/en-us/defender-cloud-apps/protect-office365#connect-microsoft-365-to-microsoft-defender-for-cloud-apps:https://learn.microsoft.com/en-us/defender-cloud-apps/protect-azure#connect-azure-to-microsoft-defender-for-cloud-apps:https://learn.microsoft.com/en-us/defender-cloud-apps/best-practices:https://learn.microsoft.com/en-us/defender-cloud-apps/get-started:https://learn.microsoft.com/en-us/entra/id-protection/concept-identity-protection-risks", + "DefaultValue": "Disabled" + } + ] + }, + { + "Id": "2.4.4", + "Description": "Zero-hour auto purge (ZAP) is a protection feature that retroactively detects and neutralizes malware and high confidence phishing. When ZAP for Teams protection blocks a message, the message is blocked for everyone in the chat. The initial block happens right after delivery, but ZAP occurs up to 48 hours after delivery.", + "Checks": [], + "Attributes": [ + { + "Section": "2 Microsoft 365 Defender", + "SubSection": "2.4 System", + "Profile": "E5 Level 1", + "AssessmentStatus": "Automated", + "Description": "Zero-hour auto purge (ZAP) is a protection feature that retroactively detects and neutralizes malware and high confidence phishing. When ZAP for Teams protection blocks a message, the message is blocked for everyone in the chat. The initial block happens right after delivery, but ZAP occurs up to 48 hours after delivery.", + "RationaleStatement": "ZAP is intended to protect users that have received zero-day malware messages or content that is weaponized after being delivered to users. It does this by continually monitoring spam and malware signatures taking automated retroactive action on messages that have already been delivered.", + "ImpactStatement": "As with any anti-malware or anti-phishing product, false positives may occur.", + "RemediationProcedure": "To remediate using the UI: 1. Navigate to Microsoft Defender https://security.microsoft.com/. 2. Click to expand System select Settings > Email & collaboration > Microsoft Teams protection. 3. Set Zero-hour auto purge (ZAP) to On (Default).", + "AuditProcedure": "To audit using the UI: 1. Navigate to Microsoft Defender https://security.microsoft.com/. 2. Click to expand System select Settings > Email & collaboration > Microsoft Teams protection. 3. Ensure Zero-hour auto purge (ZAP) is set to On (Default). 4. Under Exclude these participants review the list of exclusions and ensure they are justified and within tolerance for the organization.", + "AdditionalInformation": "", + "References": "https://learn.microsoft.com/en-us/defender-office-365/zero-hour-auto-purge?view=o365-worldwide#zero-hour-auto-purge-zap-in-microsoft-teams:https://learn.microsoft.com/en-us/defender-office-365/mdo-support-teams-about?view=o365-worldwide", + "DefaultValue": "On (Default)" + } + ] + }, + { + "Id": "3.1.1", + "Description": "When audit log search is enabled in the Microsoft Purview compliance portal, user and admin activity within the organization is recorded in the audit log and retained for 180 days by default.", + "Checks": [ + "purview_audit_log_search_enabled" + ], + "Attributes": [ + { + "Section": "3 Microsoft Purview", + "SubSection": "3.1 Audit", + "Profile": "E3 Level 1", + "AssessmentStatus": "Automated", + "Description": "When audit log search is enabled in the Microsoft Purview compliance portal, user and admin activity within the organization is recorded in the audit log and retained for 180 days by default.", + "RationaleStatement": "Enabling audit log search in the Microsoft Purview compliance portal can help organizations improve their security posture, meet regulatory compliance requirements, respond to security incidents, and gain valuable operational insights.", + "ImpactStatement": "No significant impact. Enabling audit logging is a security best practice.", + "RemediationProcedure": "To remediate using the UI: 1. Navigate to Microsoft Purview https://purview.microsoft.com/. 2. Select Solutions and then Audit to open the audit search. 3. Click blue bar Start recording user and admin activity. 4. Click Yes on the dialog box to confirm.", + "AuditProcedure": "To audit using the UI: 1. Navigate to Microsoft Purview https://purview.microsoft.com/. 2. Select Solutions and then Audit to open the audit search. 3. Choose a date and time frame in the past 30 days. 4. Verify search capabilities work and results are displayed.", + "AdditionalInformation": "", + "References": "https://learn.microsoft.com/en-us/purview/audit-log-enable-disable?view=o365-worldwide:https://learn.microsoft.com/en-us/powershell/module/exchange/set-adminauditlogconfig?view=exchange-ps", + "DefaultValue": "180 days retention" + } + ] + }, + { + "Id": "3.2.1", + "Description": "Data Loss Prevention (DLP) policies allow Exchange Online and SharePoint Online content to be scanned for specific types of data like social security numbers, credit card numbers, or passwords.", + "Checks": [], + "Attributes": [ + { + "Section": "3 Microsoft Purview", + "SubSection": "3.2 Data loss protection", + "Profile": "E3 Level 1", + "AssessmentStatus": "Automated", + "Description": "Data Loss Prevention (DLP) policies allow Exchange Online and SharePoint Online content to be scanned for specific types of data like social security numbers, credit card numbers, or passwords.", + "RationaleStatement": "Enabling DLP policies alerts users and administrators that specific types of data should not be exposed, helping to protect the data from accidental exposure.", + "ImpactStatement": "Enabling a Teams DLP policy will allow sensitive data in Exchange Online and SharePoint Online to be detected or blocked. Always ensure to follow appropriate procedures during testing and implementation of DLP policies based on organizational standards.", + "RemediationProcedure": "To remediate using the UI: 1. Navigate to Microsoft Purview https://purview.microsoft.com/. 2. Click Solutions > Data loss prevention then Policies. 3. Click Create policy. 4. Create a policy that is specific to the types of data the organization wishes to protect.", + "AuditProcedure": "To audit using the UI: 1. Navigate to Microsoft Purview https://purview.microsoft.com/. 2. Click Solutions > Data loss prevention and then Policies. 3. Verify that the organization is using policies applicable to the types data that is in their interest to protect. 4. Verify the policies are On.", + "AdditionalInformation": "The types of policies an organization should implement to protect information are specific to their industry.", + "References": "https://learn.microsoft.com/en-us/purview/dlp-learn-about-dlp?view=o365-worldwide", + "DefaultValue": "DLP policies are not configured by default." + } + ] + }, + { + "Id": "3.2.2", + "Description": "The default Teams Data Loss Prevention (DLP) policy rule in Microsoft 365 is a preconfigured rule that is automatically applied to all Teams conversations and channels. The default rule helps prevent accidental sharing of sensitive information by detecting and blocking certain types of content.", + "Checks": [], + "Attributes": [ + { + "Section": "3 Microsoft Purview", + "SubSection": "3.2 Data loss protection", + "Profile": "E5 Level 1", + "AssessmentStatus": "Automated", + "Description": "The default Teams Data Loss Prevention (DLP) policy rule in Microsoft 365 is a preconfigured rule that is automatically applied to all Teams conversations and channels. The default rule helps prevent accidental sharing of sensitive information by detecting and blocking certain types of content.", + "RationaleStatement": "Enabling the default Teams DLP policy rule in Microsoft 365 helps protect an organization's sensitive information by preventing accidental sharing or leakage Credit Card information in Teams conversations and channels.", + "ImpactStatement": "End-users may be prevented from sharing certain types of content, which may require them to adjust their behavior or seek permission from administrators to share specific content.", + "RemediationProcedure": "To remediate using the UI: 1. Navigate to Microsoft Purview compliance portal https://purview.microsoft.com/. 2. Under Solutions select Data loss prevention then Policies. 3. Click Policies tab. 4. Check Default policy for Teams then click Edit policy. 5. At the Choose locations to apply the policy page, turn the status toggle to On for Teams chat and channel messages location. 6. On Policy mode page, select Turn it on right away and click Next. 7. Review and submit.", + "AuditProcedure": "To audit using the UI: 1. Navigate to Microsoft Purview compliance portal https://purview.microsoft.com/. 2. Under Solutions select Data loss prevention then Policies. 3. Locate the Default policy for Teams. 4. Verify the Status is On. 5. Verify Locations include Teams chat and channel messages - All accounts.", + "AdditionalInformation": "Some tenants may not have a default policy for teams as Microsoft started creating these by default at a particular point in time.", + "References": "https://learn.microsoft.com/en-us/purview/dlp-teams-default-policy:https://learn.microsoft.com/en-us/powershell/module/exchange/connect-ippssession?view=exchange-ps", + "DefaultValue": "Enabled (On)" + } + ] + }, + { + "Id": "3.3.1", + "Description": "Sensitivity labels enable organizations to classify and label content across Microsoft 365 based on its sensitivity and business impact. These labels can be applied manually by users or automatically based on the content. When applied, labels can automatically encrypt content, provide Confidential watermarks, restrict access, and offer various data protection features.", + "Checks": [], + "Attributes": [ + { + "Section": "3 Microsoft Purview", + "SubSection": "3.3 Information Protection", + "Profile": "E3 Level 1", + "AssessmentStatus": "Automated", + "Description": "Sensitivity labels enable organizations to classify and label content across Microsoft 365 based on its sensitivity and business impact. These labels can be applied manually by users or automatically based on the content. When applied, labels can automatically encrypt content, provide Confidential watermarks, restrict access, and offer various data protection features.", + "RationaleStatement": "Consistent usage of sensitivity labels can help reduce the risk of data loss or exposure and enable more effective incident response if a breach does occur. They can also help organizations comply with regulatory requirements and provide visibility and control over sensitive information.", + "ImpactStatement": "Encryption configurations in the individual labels may impact users' ability to access site documents and information. Careful consideration of the individual sensitivity label configurations should be exercised prior to applying an auto labeling policy.", + "RemediationProcedure": "To remediate using the UI: 1. Navigate to Microsoft Purview compliance portal https://purview.microsoft.com/. 2. Select Information protection > Sensitivity labels. 3. Click Create a label to create a label. 4. Click Publish labels and select any newly created labels to publish according to the organization's information protection needs.", + "AuditProcedure": "To audit using the UI: 1. Navigate to Microsoft Purview compliance portal https://purview.microsoft.com/. 2. Select Information protection > Policies > Label publishing policies. 3. Ensure that a Label policy exists and is published according to the organization's information protection needs.", + "AdditionalInformation": "These policies are specific to the information protection needs of each organization.", + "References": "https://learn.microsoft.com/en-us/purview/sensitivity-labels:https://learn.microsoft.com/en-us/purview/create-sensitivity-labels", + "DefaultValue": "The Global sensitivity label policy exists by default." + } + ] + }, + { + "Id": "4.1", + "Description": "Compliance policies are sets of rules and conditions that are used to evaluate the configuration of managed devices. These policies can help secure organizational data and resources from devices that don't meet those configuration requirements. The recommended state is Mark devices with no compliance policy assigned as Not compliant.", + "Checks": [], + "Attributes": [ + { + "Section": "4 Microsoft Intune admin center", + "SubSection": "", + "Profile": "E3 Level 2", + "AssessmentStatus": "Automated", + "Description": "Compliance policies are sets of rules and conditions that are used to evaluate the configuration of managed devices. These policies can help secure organizational data and resources from devices that don't meet those configuration requirements. The recommended state is Mark devices with no compliance policy assigned as Not compliant.", + "RationaleStatement": "Implementing this setting is a first step in adopting compliance policies for devices. When used in together with Conditional Access policies the attack surface can be reduced by forcing an action to be taken for non-compliant devices.", + "ImpactStatement": "Any devices without a compliance policy will be marked not compliant. Care should be taken to first deploy any new compliance policies with a Conditional Access policy that is in the Report-only state.", + "RemediationProcedure": "To remediate using the UI: 1. Navigate to Microsoft Intune admin center https://intune.microsoft.com/. 2. Select Devices and then under Manage devices click Compliance. 3. Click Compliance settings. 4. Set Mark devices with no compliance policy assigned as to Not compliant.", + "AuditProcedure": "To audit using the UI: 1. Navigate to Microsoft Intune admin center https://intune.microsoft.com/. 2. Select Devices and then under Manage devices click Compliance. 3. Click Compliance settings. 4. Ensure Mark devices with no compliance policy assigned as is set to Not compliant.", + "AdditionalInformation": "This section does not focus on which compliance policies to use, only that an organization should adopt and enforce them to their needs.", + "References": "https://learn.microsoft.com/en-us/mem/intune/protect/device-compliance-get-started", + "DefaultValue": "UI: Compliant, Graph: secureByDefault = false" + } + ] + }, + { + "Id": "4.2", + "Description": "Device enrollment restrictions let you restrict devices from enrolling in Intune based on certain device attributes such as device limit, device platform, OS Version, manufacturer or device ownership. The recommended state is to Block personally owned devices from enrollment.", + "Checks": [], + "Attributes": [ + { + "Section": "4 Microsoft Intune admin center", + "SubSection": "", + "Profile": "E3 Level 2", + "AssessmentStatus": "Automated", + "Description": "Device enrollment restrictions let you restrict devices from enrolling in Intune based on certain device attributes such as device limit, device platform, OS Version, manufacturer or device ownership. The recommended state is to Block personally owned devices from enrollment.", + "RationaleStatement": "Restricting the enrollment of personally owned devices prevents attackers who have bypassed other controls from registering a new device to gain an additional foothold, further hiding or obscuring their activities.", + "ImpactStatement": "Per platform personally owned device enrollment impacts are listed. Windows, macOS, iOS/iPadOS, and Android devices have specific requirements for corporate enrollment. It is important to test the changes to the defaults prior to moving into production.", + "RemediationProcedure": "To remediate using the UI: 1. Navigate to Microsoft Intune admin center https://intune.microsoft.com/. 2. Select Devices and then under Device onboarding click Enrollment. 3. Under Enrollment options select Device platform restriction. 4. For the Default priority policy, click All Users and select Properties. 5. Click Edit to change Platform settings. 6. In the Personally owned column set each platform to Block.", + "AuditProcedure": "To audit using the UI: 1. Navigate to Microsoft Intune admin center https://intune.microsoft.com/. 2. Select Devices and then under Device onboarding click Enrollment. 3. Under Enrollment options select Device platform restriction. 4. Inspect the policies listed under Device type restrictions. 5. Ensure all platforms are set to Block in the Personally owned column.", + "AdditionalInformation": "Blocking platforms that are not used in the organization is a more restrictive best practice.", + "References": "https://learn.microsoft.com/en-us/mem/intune/enrollment/enrollment-restrictions-set", + "DefaultValue": "Allow" + } + ] + }, + { + "Id": "5.1.2.1", + "Description": "Legacy per-user Multi-Factor Authentication (MFA) can be configured to require individual users to provide multiple authentication factors. It was introduced in earlier versions of Office 365, prior to the more comprehensive implementation of Conditional Access (CA). The recommended state is to disable per-user MFA on all accounts.", + "Checks": [ + "entra_users_mfa_enabled" + ], + "Attributes": [ + { + "Section": "5 Microsoft Entra admin center", + "SubSection": "5.1.2 Users", + "Profile": "E3 Level 1", + "AssessmentStatus": "Automated", + "Description": "Legacy per-user Multi-Factor Authentication (MFA) can be configured to require individual users to provide multiple authentication factors. It was introduced in earlier versions of Office 365, prior to the more comprehensive implementation of Conditional Access (CA). The recommended state is to disable per-user MFA on all accounts.", + "RationaleStatement": "Both security defaults and conditional access with security defaults turned off are not compatible with per-user multi-factor authentication (MFA), which can lead to undesirable user authentication states. The CIS Microsoft 365 Benchmark explicitly employs Conditional Access for MFA as an enhancement over security defaults and as a replacement for the outdated per-user MFA.", + "ImpactStatement": "Accounts using per-user MFA will need to be migrated to use CA. Prior to disabling per-user MFA the organization must be prepared to implement conditional access MFA to avoid security gaps.", + "RemediationProcedure": "To remediate using the UI: 1. Navigate to Microsoft Entra admin center https://entra.microsoft.com/. 2. Click to expand Entra ID > Users select All users. 3. Click on Per-user MFA on the top row. 4. Click the empty box next to Display Name to select all accounts. 5. On the far right under quick steps click Disable.", + "AuditProcedure": "To audit using the UI: 1. Navigate to Microsoft Entra admin center https://entra.microsoft.com/. 2. Click to expand Entra ID > Users select All users. 3. Click on Per-user MFA on the top row. 4. Ensure under the column Multi-factor Auth Status that each account is set to Disabled.", + "AdditionalInformation": "Microsoft has documentation on migrating from per-user MFA Convert users from per-user MFA to Conditional Access based MFA.", + "References": "https://learn.microsoft.com/en-us/entra/identity/authentication/howto-mfa-userstates#convert-users-from-per-user-mfa-to-conditional-access:https://learn.microsoft.com/en-us/microsoft-365/admin/security-and-compliance/set-up-multi-factor-authentication?view=o365-worldwide", + "DefaultValue": "Disabled" + } + ] + }, + { + "Id": "5.1.2.2", + "Description": "App registration allows users to register custom-developed applications for use within the directory. Third-party integrated applications connection to services should be disabled unless there is a very clear value and robust security controls are in place.", + "Checks": [], + "Attributes": [ + { + "Section": "5 Microsoft Entra admin center", + "SubSection": "5.1.2 Users", + "Profile": "E3 Level 2", + "AssessmentStatus": "Automated", + "Description": "App registration allows users to register custom-developed applications for use within the directory. Third-party integrated applications connection to services should be disabled unless there is a very clear value and robust security controls are in place.", + "RationaleStatement": "While there are legitimate uses, attackers can grant access from breached accounts to third party applications to exfiltrate data from your tenancy without having to maintain the breached account.", + "ImpactStatement": "The implementation of this change will impact both end users and administrators. End users will not be able to integrate third-party applications that they may wish to use.", + "RemediationProcedure": "To remediate using the UI: 1. Navigate to Microsoft Entra admin center https://entra.microsoft.com/. 2. Click to expand Entra ID > Users select Users settings. 3. Set Users can register applications to No. 4. Click Save.", + "AuditProcedure": "To audit using the UI: 1. Navigate to Microsoft Entra admin center https://entra.microsoft.com/. 2. Click to expand Entra ID > Users select Users settings. 3. Verify Users can register applications is set to No.", + "AdditionalInformation": "", + "References": "https://learn.microsoft.com/en-us/entra/identity-platform/how-applications-are-added", + "DefaultValue": "Yes (Users can register applications.)" + } + ] + }, + { + "Id": "5.1.2.3", + "Description": "Non-privileged users can create tenants in the Microsoft Entra ID and Microsoft Entra administration portal under Manage tenant. The recommended state is Restrict non-admin users from creating tenants set to Yes.", + "Checks": [ + "entra_policy_ensure_default_user_cannot_create_tenants" + ], + "Attributes": [ + { + "Section": "5 Microsoft Entra admin center", + "SubSection": "5.1.2 Users", + "Profile": "E3 Level 1", + "AssessmentStatus": "Automated", + "Description": "Non-privileged users can create tenants in the Microsoft Entra ID and Microsoft Entra administration portal under Manage tenant. The recommended state is Restrict non-admin users from creating tenants set to Yes.", + "RationaleStatement": "Restricting tenant creation prevents unauthorized or uncontrolled deployment of resources and ensures that the organization retains control over its infrastructure. User generation of shadow IT could lead to multiple, disjointed environments that can make it difficult for IT to manage and secure the organization's data.", + "ImpactStatement": "Non-admin users will need to contact I.T. if they have a valid reason to create a tenant.", + "RemediationProcedure": "To remediate using the UI: 1. Navigate to Microsoft Entra admin center https://entra.microsoft.com/. 2. Click to expand Entra ID > Users > User settings. 3. Set Restrict non-admin users from creating tenants to Yes then Save.", + "AuditProcedure": "To audit using the UI: 1. Navigate to Microsoft Entra admin center https://entra.microsoft.com/. 2. Click to expand Entra ID > Users > User settings. 3. Ensure Restrict non-admin users from creating tenants is set to Yes.", + "AdditionalInformation": "", + "References": "https://learn.microsoft.com/en-us/entra/fundamentals/users-default-permissions#restrict-member-users-default-permissions", + "DefaultValue": "No - Non-administrators can create tenants." + } + ] + }, + { + "Id": "5.1.2.4", + "Description": "Restrict non-privileged users from signing into the Microsoft Entra admin center. This recommendation only affects access to the web portal. It does not prevent privileged users from using other methods such as Rest API or PowerShell to obtain information.", + "Checks": [ + "entra_admin_portals_access_restriction" + ], + "Attributes": [ + { + "Section": "5 Microsoft Entra admin center", + "SubSection": "5.1.2 Users", + "Profile": "E3 Level 1", + "AssessmentStatus": "Manual", + "Description": "Restrict non-privileged users from signing into the Microsoft Entra admin center. This recommendation only affects access to the web portal. It does not prevent privileged users from using other methods such as Rest API or PowerShell to obtain information.", + "RationaleStatement": "The Microsoft Entra admin center contains sensitive data and permission settings, which are still enforced based on the user's role. However, an end user may inadvertently change properties or account settings that could result in increased administrative overhead. Additionally, a compromised end user account could be used by a malicious attacker as a means to gather additional information and escalate an attack.", + "ImpactStatement": "In the event there are resources a user owns that need to be changed in the Entra Admin center, then an administrator would need to make those changes.", + "RemediationProcedure": "To remediate using the UI: 1. Navigate to Microsoft Entra admin center https://entra.microsoft.com/. 2. Click to expand Entra ID > Users > User settings. 3. Set Restrict access to Microsoft Entra admin center to Yes then Save.", + "AuditProcedure": "To audit using the UI: 1. Navigate to Microsoft Entra admin center https://entra.microsoft.com/. 2. Click to expand Entra ID > Users > User settings. 3. Verify under the Administration center section that Restrict access to Microsoft Entra admin center is set to Yes.", + "AdditionalInformation": "Users will still be able to sign into Microsoft Entra admin center but will be unable to see directory information.", + "References": "https://learn.microsoft.com/en-us/entra/fundamentals/users-default-permissions#restrict-member-users-default-permissions", + "DefaultValue": "No - Non-administrators can access the Microsoft Entra admin center." + } + ] + }, + { + "Id": "5.1.2.5", + "Description": "The option for the user to Stay signed in, or the Keep me signed in option, will prompt a user after a successful login. When the user selects this option, a persistent refresh token is created. The refresh token lasts for 90 days by default and does not prompt for sign-in or multifactor.", + "Checks": [], + "Attributes": [ + { + "Section": "5 Microsoft Entra admin center", + "SubSection": "5.1.2 Users", + "Profile": "E3 Level 2", + "AssessmentStatus": "Manual", + "Description": "The option for the user to Stay signed in, or the Keep me signed in option, will prompt a user after a successful login. When the user selects this option, a persistent refresh token is created. The refresh token lasts for 90 days by default and does not prompt for sign-in or multifactor.", + "RationaleStatement": "Allowing users to select this option presents risk, especially if the user signs into their account on a publicly accessible computer/web browser. In this case it would be trivial for an unauthorized person to gain access to any associated cloud data from that account.", + "ImpactStatement": "Once this setting is hidden users will no longer be prompted upon sign-in with the message Stay signed in?. This may mean users will be forced to sign in more frequently. Some features of SharePoint Online and Office 2010 have a dependency on users remaining signed in.", + "RemediationProcedure": "To remediate using the UI: 1. Navigate to Microsoft Entra admin center https://entra.microsoft.com/. 2. Click to expand Entra ID > Users > User settings. 3. Set Show keep user signed in to No. 4. Click Save.", + "AuditProcedure": "To audit using the UI: 1. Navigate to Microsoft Entra admin center https://entra.microsoft.com/. 2. Click to expand Entra ID > Users > User settings. 3. Ensure Show keep user signed in is highlighted No.", + "AdditionalInformation": "", + "References": "https://learn.microsoft.com/en-us/entra/identity/authentication/concepts-azure-multi-factor-authentication-prompts-session-lifetime:https://learn.microsoft.com/en-us/entra/fundamentals/how-to-manage-stay-signed-in-prompt", + "DefaultValue": "Users may select stay signed in" + } + ] + }, + { + "Id": "5.1.2.6", + "Description": "LinkedIn account connections allow users to connect their Microsoft work or school account with LinkedIn. After a user connects their accounts, information and highlights from LinkedIn are available in some Microsoft apps and services.", + "Checks": [], + "Attributes": [ + { + "Section": "5 Microsoft Entra admin center", + "SubSection": "5.1.2 Users", + "Profile": "E3 Level 2", + "AssessmentStatus": "Manual", + "Description": "LinkedIn account connections allow users to connect their Microsoft work or school account with LinkedIn. After a user connects their accounts, information and highlights from LinkedIn are available in some Microsoft apps and services.", + "RationaleStatement": "Disabling LinkedIn integration prevents potential phishing attacks and risk scenarios where an external party could accidentally disclose sensitive information.", + "ImpactStatement": "Users will not be able to sync contacts or use LinkedIn integration.", + "RemediationProcedure": "To remediate using the UI: 1. Navigate to Microsoft Entra admin center https://entra.microsoft.com/. 2. Click to expand Entra ID > Users select User settings. 3. Under LinkedIn account connections select No. 4. Click Save.", + "AuditProcedure": "To audit using the UI: 1. Navigate to Microsoft Entra admin center https://entra.microsoft.com/. 2. Click to expand Entra ID > Users select User settings. 3. Under LinkedIn account connections ensure No is highlighted.", + "AdditionalInformation": "", + "References": "https://learn.microsoft.com/en-us/entra/identity/users/linkedin-integration:https://learn.microsoft.com/en-us/entra/identity/users/linkedin-user-consent", + "DefaultValue": "LinkedIn integration is enabled by default." + } + ] + }, + { + "Id": "5.1.3.1", + "Description": "A dynamic group is a dynamic configuration of security group membership for Microsoft Entra ID. Administrators can set rules to populate groups that are created in Entra ID based on user attributes. The recommended state is to create a dynamic group that includes guest accounts.", + "Checks": [ + "entra_dynamic_group_for_guests_created" + ], + "Attributes": [ + { + "Section": "5 Microsoft Entra admin center", + "SubSection": "5.1.3 Groups", + "Profile": "E3 Level 1", + "AssessmentStatus": "Automated", + "Description": "A dynamic group is a dynamic configuration of security group membership for Microsoft Entra ID. Administrators can set rules to populate groups that are created in Entra ID based on user attributes. The recommended state is to create a dynamic group that includes guest accounts.", + "RationaleStatement": "Dynamic groups allow for an automated method to assign group membership. Guest user accounts will be automatically added to this group and through this existing conditional access rules, access controls and other security measures will ensure that new guest accounts are restricted in the same manner as existing guest accounts.", + "ImpactStatement": "No significant negative impact. This improves visibility and management of guest accounts.", + "RemediationProcedure": "To remediate using the UI: 1. Navigate to Microsoft Entra admin center https://entra.microsoft.com/. 2. Click to expand Entra ID > Groups select All groups. 3. Select New group and assign the following values: Group type: Security, Membership type: Dynamic User. 4. Select Add dynamic query. 5. Above the Rule syntax text box, select Edit. 6. Place the following expression in the box: (user.userType -eq \"Guest\"). 7. Select OK and Save.", + "AuditProcedure": "To audit using the UI: 1. Navigate to Microsoft Entra admin center https://entra.microsoft.com/. 2. Click to expand Entra ID > Groups select All groups. 3. On the right of the search field click Add filter. 4. Set Filter to Membership type and Value to Dynamic then apply. 5. Identify a dynamic group and select it. 6. Under manage, select Dynamic membership rules and ensure the rule syntax contains (user.userType -eq \"Guest\").", + "AdditionalInformation": "", + "References": "https://learn.microsoft.com/en-us/entra/identity/users/groups-create-rule:https://learn.microsoft.com/en-us/entra/identity/users/groups-dynamic-membership:https://learn.microsoft.com/en-us/entra/external-id/use-dynamic-groups", + "DefaultValue": "Undefined" + } + ] + }, + { + "Id": "5.1.3.2", + "Description": "This setting allows users in the organization to create new security groups and add members to these groups in the Azure portal, API, or PowerShell. The recommended state is Users can create security groups in Azure portals, API or PowerShell set to No.", + "Checks": [], + "Attributes": [ + { + "Section": "5 Microsoft Entra admin center", + "SubSection": "5.1.3 Groups", + "Profile": "E3 Level 1", + "AssessmentStatus": "Automated", + "Description": "This setting allows users in the organization to create new security groups and add members to these groups in the Azure portal, API, or PowerShell. The recommended state is Users can create security groups in Azure portals, API or PowerShell set to No.", + "RationaleStatement": "Allowing end users to create security groups without oversight can lead to uncontrolled group sprawl, increasing the risk of inappropriate access to sensitive data. A compromised non-privileged user could create deceptively named security groups that an administrator might mistakenly assign elevated privileges to.", + "ImpactStatement": "Restrictions may introduce some operational friction, particularly in fast-paced or decentralized environments where teams rely on self-service capabilities for collaboration and access management.", + "RemediationProcedure": "To remediate using the UI: 1. Navigate to Microsoft Entra admin center https://entra.microsoft.com/. 2. Click to expand Entra ID > Groups select General. 3. Set Users can create security groups in Azure portals, API or PowerShell to No.", + "AuditProcedure": "To audit using the UI: 1. Navigate to Microsoft Entra admin center https://entra.microsoft.com/. 2. Click to expand Entra ID > Groups select General. 3. Ensure 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:https://learn.microsoft.com/en-us/graph/api/authorizationpolicy-get?view=graph-rest-1.0", + "DefaultValue": "AllowedToCreateSecurityGroups: True" + } + ] + }, + { + "Id": "5.1.4.1", + "Description": "This setting enables you to select the users who can register their devices as Microsoft Entra joined devices. The recommended state is Selected or None.", + "Checks": [], + "Attributes": [ + { + "Section": "5 Microsoft Entra admin center", + "SubSection": "5.1.4 Devices", + "Profile": "E3 Level 2", + "AssessmentStatus": "Automated", + "Description": "This setting enables you to select the users who can register their devices as Microsoft Entra joined devices. The recommended state is Selected or None.", + "RationaleStatement": "If a threat actor compromises a standard user account, they can enroll a rogue device under that user's identity. This device may inherit MDM policies and appear compliant, giving attackers persistent access to cloud resources without triggering MFA.", + "ImpactStatement": "Restricting the setting requires IT teams to assign enrollment permissions to specific staff, such as helpdesk or provisioning personnel, which may impact user-driven Autopilot scenarios and increase administrative overhead.", + "RemediationProcedure": "To remediate using the UI: 1. Navigate to Microsoft Entra admin center https://entra.microsoft.com/. 2. Click to expand Entra ID > Devices select Device settings. 3. Set Users may join devices to Microsoft Entra to Selected (and add members) or None.", + "AuditProcedure": "To audit using the UI: 1. Navigate to Microsoft Entra admin center https://entra.microsoft.com/. 2. Click to expand Entra ID > Devices select Device settings. 3. Ensure Users may join devices to Microsoft Entra is set to Selected or None.", + "AdditionalInformation": "This setting is applicable only to Microsoft Entra join on Windows 10 or newer.", + "References": "https://learn.microsoft.com/en-us/entra/identity/devices/manage-device-identities#configure-device-settings", + "DefaultValue": "All" + } + ] + }, + { + "Id": "5.1.4.2", + "Description": "This setting defines the maximum number of Microsoft Entra joined or registered devices that a user can have in Microsoft Entra ID. Once this limit is reached, no additional devices can be added until existing ones are removed. The recommended state is 20 or less.", + "Checks": [], + "Attributes": [ + { + "Section": "5 Microsoft Entra admin center", + "SubSection": "5.1.4 Devices", + "Profile": "E3 Level 1", + "AssessmentStatus": "Automated", + "Description": "This setting defines the maximum number of Microsoft Entra joined or registered devices that a user can have in Microsoft Entra ID. Once this limit is reached, no additional devices can be added until existing ones are removed. The recommended state is 20 or less.", + "RationaleStatement": "Microsoft incident response teams have observed threat actors enrolling their own devices to establish persistence after a non-privileged user has been compromised. High device quotas can exacerbate this risk by enabling attackers to register multiple devices that appear legitimate.", + "ImpactStatement": "IT staff who need to enroll more than 20 devices on behalf of the organization must be assigned the role of Device Enrollment Manager in the Intune admin center.", + "RemediationProcedure": "To remediate using the UI: 1. Navigate to Microsoft Entra admin center https://entra.microsoft.com/. 2. Click to expand Entra ID > Devices select Device settings. 3. Set Maximum number of devices per user to 20 (Recommended) or less.", + "AuditProcedure": "To audit using the UI: 1. Navigate to Microsoft Entra admin center https://entra.microsoft.com/. 2. Click to expand Entra ID > Devices select Device settings. 3. Ensure Maximum number of devices per user is set to 20 (Recommended) or less.", + "AdditionalInformation": "Do not delete accounts assigned as a Device enrollment manager if any devices were enrolled using the account.", + "References": "https://learn.microsoft.com/en-us/entra/identity/devices/manage-device-identities#configure-device-settings:https://learn.microsoft.com/en-us/intune/intune-service/enrollment/device-enrollment-manager-enroll", + "DefaultValue": "50" + } + ] + }, + { + "Id": "5.1.4.3", + "Description": "This setting controls whether the Global Administrator role is automatically added to the local administrators group on a device during the Microsoft Entra join process. The recommended state is No.", + "Checks": [], + "Attributes": [ + { + "Section": "5 Microsoft Entra admin center", + "SubSection": "5.1.4 Devices", + "Profile": "E3 Level 1", + "AssessmentStatus": "Automated", + "Description": "This setting controls whether the Global Administrator role is automatically added to the local administrators group on a device during the Microsoft Entra join process. The recommended state is No.", + "RationaleStatement": "System administrators may be inclined to use over-privileged accounts for convenience when managing devices. Enforcing this control helps discourage that behavior by requiring administrative actions to be performed using accounts specifically designated for local administration.", + "ImpactStatement": "Restricting the default behavior and requiring manual assignment to least privilege roles introduces minor administrative overhead.", + "RemediationProcedure": "To remediate using the UI: 1. Navigate to Microsoft Entra admin center https://entra.microsoft.com/. 2. Click to expand Entra ID > Devices select Device settings. 3. Set Global administrator role is added as local administrator on the device during Microsoft Entra join (Preview) to No.", + "AuditProcedure": "To audit using the UI: 1. Navigate to Microsoft Entra admin center https://entra.microsoft.com/. 2. Click to expand Entra ID > Devices select Device settings. 3. Ensure Global administrator role is added as local administrator on the device during Microsoft Entra join (Preview) is set to No.", + "AdditionalInformation": "", + "References": "https://learn.microsoft.com/en-us/entra/identity/devices/manage-device-identities#configure-device-settings:https://learn.microsoft.com/en-us/entra/identity/devices/assign-local-admin", + "DefaultValue": "Yes" + } + ] + }, + { + "Id": "5.1.4.4", + "Description": "This setting determines if the Microsoft Entra user registering their device as Microsoft Entra join be added to the local administrators group. The recommended state is Selected or None.", + "Checks": [], + "Attributes": [ + { + "Section": "5 Microsoft Entra admin center", + "SubSection": "5.1.4 Devices", + "Profile": "E3 Level 1", + "AssessmentStatus": "Automated", + "Description": "This setting determines if the Microsoft Entra user registering their device as Microsoft Entra join be added to the local administrators group. The recommended state is Selected or None.", + "RationaleStatement": "To uphold the principle of least privilege, the assignment of local administrator rights during Microsoft Entra join should be centrally managed using appropriate built-in roles through Intune.", + "ImpactStatement": "Restricting the default behavior and requiring manual assignment to built-in roles introduces minor administrative overhead.", + "RemediationProcedure": "To remediate using the UI: 1. Navigate to Microsoft Entra admin center https://entra.microsoft.com/. 2. Click to expand Entra ID > Devices select Device settings. 3. Set Registering user is added as local administrator on the device during Microsoft Entra join (Preview) to Selected (and add members) or None.", + "AuditProcedure": "To audit using the UI: 1. Navigate to Microsoft Entra admin center https://entra.microsoft.com/. 2. Click to expand Entra ID > Devices select Device settings. 3. Ensure Registering user is added as local administrator on the device during Microsoft Entra join (Preview) is set to Selected or None.", + "AdditionalInformation": "", + "References": "https://learn.microsoft.com/en-us/entra/identity/devices/manage-device-identities#configure-device-settings:https://learn.microsoft.com/en-us/entra/identity/devices/assign-local-admin", + "DefaultValue": "All" + } + ] + }, + { + "Id": "5.1.4.5", + "Description": "Local Administrator Password Solution (LAPS) is the management of local account passwords on Windows devices. LAPS provides a solution to securely manage and retrieve the built-in local admin password. The recommended state is Yes.", + "Checks": [], + "Attributes": [ + { + "Section": "5 Microsoft Entra admin center", + "SubSection": "5.1.4 Devices", + "Profile": "E3 Level 1", + "AssessmentStatus": "Automated", + "Description": "Local Administrator Password Solution (LAPS) is the management of local account passwords on Windows devices. LAPS provides a solution to securely manage and retrieve the built-in local admin password. The recommended state is Yes.", + "RationaleStatement": "Managing local Administrator passwords across multiple systems can be challenging. LAPS reduces the security risk when administrators configure the same password on all workstations.", + "ImpactStatement": "Enabling LAPS requires some additional operational overhead. Although unlikely, if a password is rotated and not retrieved before the device becomes unreachable, administrators may be locked out.", + "RemediationProcedure": "To remediate using the UI: 1. Navigate to Microsoft Entra admin center https://entra.microsoft.com/. 2. Click to expand Entra ID > Devices select Device settings. 3. Set Enable Microsoft Entra Local Administrator Password Solution (LAPS) to Yes.", + "AuditProcedure": "To audit using the UI: 1. Navigate to Microsoft Entra admin center https://entra.microsoft.com/. 2. Click to expand Entra ID > Devices select Device settings. 3. Ensure Enable Microsoft Entra Local Administrator Password Solution (LAPS) is set to Yes.", + "AdditionalInformation": "Enabling LAPS at the tenant level does not automatically enforce password rotation for built-in Administrator accounts.", + "References": "https://learn.microsoft.com/en-us/entra/identity/devices/manage-device-identities#configure-device-settings:https://learn.microsoft.com/en-us/entra/identity/devices/howto-manage-local-admin-passwords", + "DefaultValue": "No" + } + ] + }, + { + "Id": "5.1.4.6", + "Description": "This setting determines if users can self-service recover their BitLocker key(s). The recommended state is No.", + "Checks": [], + "Attributes": [ + { + "Section": "5 Microsoft Entra admin center", + "SubSection": "5.1.4 Devices", + "Profile": "E3 Level 2", + "AssessmentStatus": "Automated", + "Description": "This setting determines if users can self-service recover their BitLocker key(s). The recommended state is No.", + "RationaleStatement": "Restricting users from recovering BitLocker keys helps prevent unauthorized access to encrypted drives. If a user's account is compromised, an attacker could potentially recover BitLocker keys and access sensitive data.", + "ImpactStatement": "Users will need to contact IT support to recover BitLocker keys, which may increase support overhead.", + "RemediationProcedure": "To remediate using the UI: 1. Navigate to Microsoft Entra admin center https://entra.microsoft.com/. 2. Click to expand Entra ID > Devices select Device settings. 3. Set Restrict users from recovering the BitLocker key(s) for their owned devices to Yes.", + "AuditProcedure": "To audit using the UI: 1. Navigate to Microsoft Entra admin center https://entra.microsoft.com/. 2. Click to expand Entra ID > Devices select Device settings. 3. Ensure Restrict users from recovering the BitLocker key(s) for their owned devices is set to Yes.", + "AdditionalInformation": "", + "References": "https://learn.microsoft.com/en-us/entra/identity/devices/manage-device-identities#configure-device-settings", + "DefaultValue": "No" + } + ] + }, + { + "Id": "5.1.5.1", + "Description": "User consent to apps accessing company data on their behalf allows users to grant permissions to applications without administrator involvement. The recommended state is Do not allow user consent.", + "Checks": [ + "entra_policy_restricts_user_consent_for_apps" + ], + "Attributes": [ + { + "Section": "5 Microsoft Entra admin center", + "SubSection": "5.1.5 Applications", + "Profile": "E3 Level 2", + "AssessmentStatus": "Automated", + "Description": "User consent to apps accessing company data on their behalf allows users to grant permissions to applications without administrator involvement. The recommended state is Do not allow user consent.", + "RationaleStatement": "Attackers commonly use custom applications to trick users into granting access to company data. Disabling user consent and establishing an admin consent workflow can reduce this risk.", + "ImpactStatement": "Users will need to request administrator approval before they can use applications that require access to company data.", + "RemediationProcedure": "To remediate using the UI: 1. Navigate to Microsoft Entra admin center https://entra.microsoft.com/. 2. Click to expand Entra ID > Applications > Enterprise applications > Consent and permissions. 3. Set User consent for applications to Do not allow user consent. 4. Click Save.", + "AuditProcedure": "To audit using the UI: 1. Navigate to Microsoft Entra admin center https://entra.microsoft.com/. 2. Click to expand Entra ID > Applications > Enterprise applications > Consent and permissions. 3. Ensure User consent for applications is set to Do not allow user consent.", + "AdditionalInformation": "", + "References": "https://learn.microsoft.com/en-us/entra/identity/enterprise-apps/configure-user-consent", + "DefaultValue": "Allow user consent for apps from verified publishers" + } + ] + }, + { + "Id": "5.1.5.2", + "Description": "The admin consent workflow gives users a way to request access to applications that require admin consent. The recommended state is Enable the admin consent workflow.", + "Checks": [ + "entra_admin_consent_workflow_enabled" + ], + "Attributes": [ + { + "Section": "5 Microsoft Entra admin center", + "SubSection": "5.1.5 Applications", + "Profile": "E3 Level 1", + "AssessmentStatus": "Automated", + "Description": "The admin consent workflow gives users a way to request access to applications that require admin consent. The recommended state is Enable the admin consent workflow.", + "RationaleStatement": "The admin consent workflow provides a secure method for users to request access to applications that require permissions. This ensures that administrators can review and approve requests before users gain access.", + "ImpactStatement": "Administrators will need to review and approve consent requests, which may increase administrative overhead.", + "RemediationProcedure": "To remediate using the UI: 1. Navigate to Microsoft Entra admin center https://entra.microsoft.com/. 2. Click to expand Entra ID > Applications > Enterprise applications > Consent and permissions > Admin consent settings. 3. Set Users can request admin consent to apps they are unable to consent to to Yes. 4. Configure reviewers and notification settings. 5. Click Save.", + "AuditProcedure": "To audit using the UI: 1. Navigate to Microsoft Entra admin center https://entra.microsoft.com/. 2. Click to expand Entra ID > Applications > Enterprise applications > Consent and permissions > Admin consent settings. 3. Ensure Users can request admin consent to apps they are unable to consent to is set to Yes.", + "AdditionalInformation": "", + "References": "https://learn.microsoft.com/en-us/entra/identity/enterprise-apps/configure-admin-consent-workflow", + "DefaultValue": "No" + } + ] + }, + { + "Id": "5.1.6.1", + "Description": "External collaboration settings allow you to specify which domains users can invite for B2B collaboration. The recommended state is to send collaboration invitations to allowed domains only.", + "Checks": [ + "entra_thirdparty_integrated_apps_not_allowed" + ], + "Attributes": [ + { + "Section": "5 Microsoft Entra admin center", + "SubSection": "5.1.6 External Identities", + "Profile": "E3 Level 2", + "AssessmentStatus": "Automated", + "Description": "External collaboration settings allow you to specify which domains users can invite for B2B collaboration. The recommended state is to send collaboration invitations to allowed domains only.", + "RationaleStatement": "Restricting invitations to allowed domains helps prevent unauthorized access to your organization's resources. This reduces the risk of data exposure to untrusted external parties.", + "ImpactStatement": "Users will only be able to invite guests from specified domains, which may limit collaboration with partners from unapproved domains.", + "RemediationProcedure": "To remediate using the UI: 1. Navigate to Microsoft Entra admin center https://entra.microsoft.com/. 2. Click to expand Entra ID > External Identities > External collaboration settings. 3. Under Collaboration restrictions, select Allow invitations only to the specified domains. 4. Add the allowed domains. 5. Click Save.", + "AuditProcedure": "To audit using the UI: 1. Navigate to Microsoft Entra admin center https://entra.microsoft.com/. 2. Click to expand Entra ID > External Identities > External collaboration settings. 3. Ensure Allow invitations only to the specified domains is selected under Collaboration restrictions.", + "AdditionalInformation": "", + "References": "https://learn.microsoft.com/en-us/entra/external-id/allow-deny-list", + "DefaultValue": "Allow invitations to be sent to any domain" + } + ] + }, + { + "Id": "5.1.6.2", + "Description": "Guest user access restrictions determine the level of access that guest users have in your directory. The recommended state is Guest user access is restricted to properties and memberships of their own directory objects.", + "Checks": [ + "entra_policy_guest_users_access_restrictions" + ], + "Attributes": [ + { + "Section": "5 Microsoft Entra admin center", + "SubSection": "5.1.6 External Identities", + "Profile": "E3 Level 1", + "AssessmentStatus": "Automated", + "Description": "Guest user access restrictions determine the level of access that guest users have in your directory. The recommended state is Guest user access is restricted to properties and memberships of their own directory objects.", + "RationaleStatement": "Restricting guest user access helps prevent unauthorized enumeration of directory information. This reduces the risk of reconnaissance attacks by external users.", + "ImpactStatement": "Guest users will have limited visibility into directory objects, which may affect collaboration scenarios that require directory lookups.", + "RemediationProcedure": "To remediate using the UI: 1. Navigate to Microsoft Entra admin center https://entra.microsoft.com/. 2. Click to expand Entra ID > External Identities > External collaboration settings. 3. Under Guest user access, select Guest user access is restricted to properties and memberships of their own directory objects. 4. Click Save.", + "AuditProcedure": "To audit using the UI: 1. Navigate to Microsoft Entra admin center https://entra.microsoft.com/. 2. Click to expand Entra ID > External Identities > External collaboration settings. 3. Ensure Guest user access is restricted to properties and memberships of their own directory objects is selected.", + "AdditionalInformation": "", + "References": "https://learn.microsoft.com/en-us/entra/fundamentals/users-default-permissions#restrict-member-users-default-permissions", + "DefaultValue": "Guest users have the same access as members" + } + ] + }, + { + "Id": "5.1.6.3", + "Description": "Guest invite settings determine who can invite external users to collaborate. The recommended state is to limit invitations to the Guest Inviter role.", + "Checks": [ + "entra_policy_guest_invite_only_for_admin_roles" + ], + "Attributes": [ + { + "Section": "5 Microsoft Entra admin center", + "SubSection": "5.1.6 External Identities", + "Profile": "E3 Level 2", + "AssessmentStatus": "Automated", + "Description": "Guest invite settings determine who can invite external users to collaborate. The recommended state is to limit invitations to the Guest Inviter role.", + "RationaleStatement": "Restricting guest invitations to specific roles helps maintain control over who can add external users to your organization. This reduces the risk of unauthorized guest accounts being created.", + "ImpactStatement": "Only users with the Guest Inviter role will be able to invite external users, which may require role assignment changes for collaboration scenarios.", + "RemediationProcedure": "To remediate using the UI: 1. Navigate to Microsoft Entra admin center https://entra.microsoft.com/. 2. Click to expand Entra ID > External Identities > External collaboration settings. 3. Under Guest invite settings, select Only users assigned to specific admin roles can invite guest users. 4. Click Save.", + "AuditProcedure": "To audit using the UI: 1. Navigate to Microsoft Entra admin center https://entra.microsoft.com/. 2. Click to expand Entra ID > External Identities > External collaboration settings. 3. Ensure Only users assigned to specific admin roles can invite guest users is selected.", + "AdditionalInformation": "", + "References": "https://learn.microsoft.com/en-us/entra/external-id/external-collaboration-settings-configure", + "DefaultValue": "Anyone in the organization can invite guest users" + } + ] + }, + { + "Id": "5.1.8.1", + "Description": "Password hash sync is enabled for hybrid deployments to ensure that on-premises password changes are synchronized to Microsoft Entra ID. The recommended state is to enable password hash sync.", + "Checks": [ + "entra_password_hash_sync_enabled" + ], + "Attributes": [ + { + "Section": "5 Microsoft Entra admin center", + "SubSection": "5.1.8 Hybrid management", + "Profile": "E3 Level 1", + "AssessmentStatus": "Manual", + "Description": "Password hash sync is enabled for hybrid deployments to ensure that on-premises password changes are synchronized to Microsoft Entra ID. The recommended state is to enable password hash sync.", + "RationaleStatement": "Password hash synchronization is one of the sign-in methods used for hybrid identity. It provides a backup authentication method if federation services become unavailable and enables leaked credential detection through Entra ID Protection.", + "ImpactStatement": "Enabling password hash sync requires configuration of Microsoft Entra Connect and may have implications for compliance in regulated industries.", + "RemediationProcedure": "To remediate: 1. Open Microsoft Entra Connect on your synchronization server. 2. Select Configure and then Change user sign-in. 3. Enable Password Hash Synchronization. 4. Complete the wizard.", + "AuditProcedure": "To audit using the UI: 1. Navigate to Microsoft Entra admin center https://entra.microsoft.com/. 2. Click to expand Entra ID > Hybrid management > Microsoft Entra Connect. 3. Verify that Password Hash Sync is enabled.", + "AdditionalInformation": "This recommendation applies only to hybrid environments.", + "References": "https://learn.microsoft.com/en-us/entra/identity/hybrid/connect/whatis-phs", + "DefaultValue": "Disabled" + } + ] + }, + { + "Id": "5.2.2.1", + "Description": "Multifactor authentication is a process that requires an additional form of identification during the sign-in process, such as a code from a mobile device or a fingerprint scan, to enhance security. Ensure users in administrator roles have MFA capabilities enabled.", + "Checks": [ + "entra_admin_users_mfa_enabled" + ], + "Attributes": [ + { + "Section": "5 Microsoft Entra admin center", + "SubSection": "5.2.2 Risk-based Conditional Access", + "Profile": "E3 Level 1", + "AssessmentStatus": "Automated", + "Description": "Multifactor authentication is a process that requires an additional form of identification during the sign-in process, such as a code from a mobile device or a fingerprint scan, to enhance security. Ensure users in administrator roles have MFA capabilities enabled.", + "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": "Implementation of multifactor authentication for all users in administrative roles will necessitate a change to user routine. All users in administrative roles will be required to enroll in multifactor authentication using phone, SMS, or an authentication application. After enrollment, use of multifactor authentication will be required for future access to the environment.", + "RemediationProcedure": "To remediate using the UI: 1. Navigate to the Microsoft Entra admin center https://entra.microsoft.com. 2. Click expand ID Protection > Risk-based Conditional Access. 3. Click New policy. Under Users include Select users and groups and check Directory roles. At a minimum, include the directory roles: Application administrator, Authentication administrator, Billing administrator, Cloud application administrator, Conditional Access administrator, Exchange administrator, Global administrator, Global reader, Helpdesk administrator, Password administrator, Privileged authentication administrator, Privileged role administrator, Security administrator, SharePoint administrator, User administrator. Under Target resources include All resources (formerly 'All cloud apps') and do not create any exclusions. Under Grant select Grant Access and check either Require multifactor authentication or Require authentication strength. Click Select at the bottom of the pane. 4. Under Enable policy set it to Report-only until the organization is ready to enable it. 5. Click Create.", + "AuditProcedure": "To audit using the UI: 1. Navigate to the Microsoft Entra admin center https://entra.microsoft.com. 2. Click expand ID Protection > Risk-based Conditional Access. 3. Ensure that a policy exists with the following criteria and is set to On: Under Users verify Directory roles specific to administrators are included. Ensure that only documented user exclusions exist and that they are reviewed annually. Under Target resources verify All resources (formerly 'All cloud apps') is selected with no exclusions. Under Grant verify Grant Access is on and either Require multifactor authentication or Require authentication strength is checked. 4. Ensure Enable policy is set to On.", + "AdditionalInformation": "Break-glass accounts should be excluded from all Conditional Access policies.", + "References": "https://learn.microsoft.com/en-us/entra/identity/conditional-access/howto-conditional-access-policy-admin-mfa", + "DefaultValue": "MFA is not enabled by default for administrator roles" + } + ] + }, + { + "Id": "5.2.2.2", + "Description": "Enable multifactor authentication for all users in the Microsoft 365 tenant. Users will be prompted to authenticate with a second factor upon logging in to Microsoft 365 services.", + "Checks": [ + "entra_users_mfa_enabled" + ], + "Attributes": [ + { + "Section": "5 Microsoft Entra admin center", + "SubSection": "5.2.2 Risk-based Conditional Access", + "Profile": "E3 Level 1", + "AssessmentStatus": "Automated", + "Description": "Enable multifactor authentication for all users in the Microsoft 365 tenant. Users will be prompted to authenticate with a second factor upon logging in to Microsoft 365 services.", + "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": "Implementation of multifactor authentication for all users will necessitate a change to user routine. All users will be required to enroll in multifactor authentication using phone, SMS, or an authentication application. After enrollment, use of multifactor authentication will be required for future authentication to the environment. External identities that attempt to access documents that utilize Purview Information Protection (Sensitivity Labels) will find their access disrupted. In order to mitigate this create an exclusion for Microsoft Rights Management Services ID: 00000012-0000-0000-c000-000000000000.", + "RemediationProcedure": "To remediate using the UI: 1. Navigate to the Microsoft Entra admin center https://entra.microsoft.com. 2. Click expand Protection > Conditional Access select Policies. 3. Click New policy. Under Users include All users. Under Target resources include All resources (formerly 'All cloud apps') and do not create any exclusions. Under Grant select Grant Access and check either Require multifactor authentication or Require authentication strength. Click Select at the bottom of the pane. 4. Under Enable policy set it to Report-only until the organization is ready to enable it. 5. Click Create.", + "AuditProcedure": "To audit using the UI: 1. Navigate to the Microsoft Entra admin center https://entra.microsoft.com. 2. Click expand ID Protection > Risk-based Conditional Access. 3. Ensure that a policy exists with the following criteria and is set to On: Under Users verify All users is included. Ensure that only documented user exclusions exist and that they are reviewed annually. Under Target resources verify All resources (formerly 'All cloud apps') is selected with no exclusions. Under Grant verify Grant Access and either Require multifactor authentication or Require authentication strength is checked. 4. Ensure Enable policy is set to On.", + "AdditionalInformation": "Break-glass accounts should be excluded from all Conditional Access policies. Organizations that struggle to enforce MFA globally due to budget constraints or regulations can use FIDO2 security keys as an alternative.", + "References": "https://learn.microsoft.com/en-us/entra/identity/conditional-access/howto-conditional-access-policy-all-users-mfa", + "DefaultValue": "MFA is not enabled by default for all users" + } + ] + }, + { + "Id": "5.2.2.3", + "Description": "Entra ID supports the most widely used authentication and authorization protocols including legacy authentication. This authentication pattern includes basic authentication, a widely used industry-standard method for collecting username and password information. Enable Conditional Access policies to block legacy authentication.", + "Checks": [ + "entra_legacy_authentication_blocked" + ], + "Attributes": [ + { + "Section": "5 Microsoft Entra admin center", + "SubSection": "5.2.2 Risk-based Conditional Access", + "Profile": "E3 Level 1", + "AssessmentStatus": "Automated", + "Description": "Entra ID supports the most widely used authentication and authorization protocols including legacy authentication. This authentication pattern includes basic authentication, a widely used industry-standard method for collecting username and password information. Enable Conditional Access policies to block legacy authentication.", + "RationaleStatement": "Legacy authentication protocols do not support multi-factor authentication. These protocols are often used by attackers because of this deficiency. Blocking legacy authentication makes it harder for attackers to gain access.", + "ImpactStatement": "Enabling this setting will prevent users from connecting with older versions of Office, ActiveSync or using protocols like IMAP, POP or SMTP and may require upgrades to older versions of Office, and use of mobile mail clients that support modern authentication. This will also cause multifunction devices such as printers from using scan to e-mail function if they are using a legacy authentication method.", + "RemediationProcedure": "To remediate using the UI: 1. Navigate to the Microsoft Entra admin center https://entra.microsoft.com. 2. Click expand ID Protection > Risk-based Conditional Access. 3. Create a new policy by selecting New policy. Under Users include All users. Under Target resources include All resources (formerly 'All cloud apps'). Under Conditions select Client apps and check the boxes for Exchange ActiveSync clients and Other clients. Under Grant select Block Access. Click Select. 4. Set the policy On and click Create.", + "AuditProcedure": "To audit using the UI: 1. Navigate to the Microsoft Entra admin center https://entra.microsoft.com. 2. Click expand ID Protection > Risk-based Conditional Access. 3. Ensure that a policy exists with the following criteria and is set to On: Under Users verify All users is included. Ensure that only documented user exclusions exist and that they are reviewed annually. Under Target resources verify All resources (formerly 'All cloud apps') is selected. Ensure that only documented resource exclusions exist and that they are reviewed annually. Under Conditions select Client apps then verify Exchange ActiveSync clients and Other clients is checked. Under Grant verify Block access is selected. 4. Ensure Enable policy is set to On.", + "AdditionalInformation": "Break-glass accounts should be excluded from all Conditional Access policies. Basic authentication is now disabled in all tenants as of January 2023.", + "References": "https://learn.microsoft.com/en-us/exchange/clients-and-mobile-in-exchange-online/disable-basic-authentication-in-exchange-online", + "DefaultValue": "Basic authentication is disabled by default as of January 2023" + } + ] + }, + { + "Id": "5.2.2.4", + "Description": "In complex deployments, organizations might have a need to restrict authentication sessions. Conditional Access policies allow for the targeting of specific user accounts. Ensure Sign-in frequency is enabled and browser sessions are not persistent for Administrative users.", + "Checks": [], + "Attributes": [ + { + "Section": "5 Microsoft Entra admin center", + "SubSection": "5.2.2 Risk-based Conditional Access", + "Profile": "E3 Level 1", + "AssessmentStatus": "Automated", + "Description": "In complex deployments, organizations might have a need to restrict authentication sessions. Conditional Access policies allow for the targeting of specific user accounts. Ensure Sign-in frequency is enabled and browser sessions are not persistent for Administrative users.", + "RationaleStatement": "Forcing a time out for MFA will help ensure that sessions are not kept alive for an indefinite period of time, ensuring that browser sessions are not persistent will help in prevention of drive-by attacks in web browsers, this also prevents creation and saving of session cookies leaving nothing for an attacker to take.", + "ImpactStatement": "Users with Administrative roles will be prompted at the frequency set for MFA.", + "RemediationProcedure": "To remediate using the UI: 1. Navigate to Microsoft Entra admin center https://entra.microsoft.com/. 2. Click expand ID Protection > Risk-based Conditional Access. 3. Click New policy. Under Users include Select users and groups and check Directory roles. At a minimum, include the directory roles: Application administrator, Authentication administrator, Billing administrator, Cloud application administrator, Conditional Access administrator, Exchange administrator, Global administrator, Global reader, Helpdesk administrator, Password administrator, Privileged authentication administrator, Privileged role administrator, Security administrator, SharePoint administrator, User administrator. Under Target resources include All resources (formerly 'All cloud apps'). Under Grant select Grant Access and check Require multifactor authentication. Under Session select Sign-in frequency select Periodic reauthentication and set it to 4 hours (or less). Check Persistent browser session then select Never persistent in the drop-down menu. 4. Under Enable policy set it to Report-only until the organization is ready to enable it.", + "AuditProcedure": "To audit using the UI: 1. Navigate to Microsoft Entra admin center https://entra.microsoft.com/. 2. Click expand ID Protection > Risk-based Conditional Access. 3. Ensure that a policy exists with the following criteria and is set to On: Under Users verify Directory roles specific to administrators are included. Ensure that only documented user exclusions exist and that they are reviewed annually. Under Target resources verify All resources (formerly 'All cloud apps') is selected. Ensure that only documented resource exclusions exist and that they are reviewed annually. Under Session verify Sign-in frequency is checked and set to Periodic reauthentication. Verify the timeframe is set to the time determined by the organization. Ensure Periodic reauthentication does not exceed 4 hours (or less). Verify Persistent browser session is set to Never persistent. 4. Ensure Enable policy is set to On.", + "AdditionalInformation": "Break-glass accounts should be excluded from all Conditional Access policies.", + "References": "https://learn.microsoft.com/en-us/entra/identity/conditional-access/howto-conditional-access-session-lifetime", + "DefaultValue": "The default configuration for user sign-in frequency is a rolling window of 90 days" + } + ] + }, + { + "Id": "5.2.2.5", + "Description": "Authentication strength is a Conditional Access control that allows administrators to specify which combination of authentication methods can be used to access a resource. Ensure administrator roles are using a CA policy with Phishing-resistant MFA strength.", + "Checks": [ + "entra_admin_users_phishing_resistant_mfa_enabled" + ], + "Attributes": [ + { + "Section": "5 Microsoft Entra admin center", + "SubSection": "5.2.2 Risk-based Conditional Access", + "Profile": "E3 Level 2", + "AssessmentStatus": "Automated", + "Description": "Authentication strength is a Conditional Access control that allows administrators to specify which combination of authentication methods can be used to access a resource. Ensure administrator roles are using a CA policy with Phishing-resistant MFA strength.", + "RationaleStatement": "Sophisticated attacks targeting MFA are more prevalent as the use of it becomes more widespread. These 3 methods (FIDO2 Security Key, Windows Hello for Business, Certificate-based authentication) are considered phishing-resistant as they remove passwords from the login workflow. It also ensures that public/private key exchange can only happen between the devices and a registered provider which prevents login to fake or phishing websites.", + "ImpactStatement": "If administrators aren't pre-registered for a strong authentication method prior to a conditional access policy being created, then a condition could occur where a user can't register for strong authentication because they don't meet the conditional access policy requirements and therefore are prevented from signing in. Additionally, Internet Explorer based credential prompts in PowerShell do not support prompting for a security key.", + "RemediationProcedure": "To remediate using the UI: 1. Navigate to the Microsoft Entra admin center https://entra.microsoft.com. 2. Click expand ID Protection > Risk-based Conditional Access. 3. Click New policy. Under Users include Select users and groups and check Directory roles. At a minimum, include the directory roles: Application administrator, Authentication administrator, Billing administrator, Cloud application administrator, Conditional Access administrator, Exchange administrator, Global administrator, Global reader, Helpdesk administrator, Password administrator, Privileged authentication administrator, Privileged role administrator, Security administrator, SharePoint administrator, User administrator. Under Target resources include All resources (formerly 'All cloud apps') and do not create any exclusions. Under Grant select Grant Access and check Require authentication strength and set Phishing-resistant MFA in the dropdown box. Click Select. 4. Under Enable policy set it to Report-only until the organization is ready to enable it. 5. Click Create.", + "AuditProcedure": "To audit using the UI: 1. Navigate to the Microsoft Entra admin center https://entra.microsoft.com. 2. Click expand ID Protection > Risk-based Conditional Access. 3. Ensure that a policy exists with the following criteria and is set to On: Under Users verify Directory roles specific to administrators are included. Ensure that only documented user exclusions exist and that they are reviewed annually. Directory Roles should include at minimum the roles listed in the remediation section. Under Target resources verify All resources (formerly 'All cloud apps') is selected with no exclusions. Under Grant verify Grant Access is selected and Require authentication strength is checked with Phishing-resistant MFA set as the value. 4. Ensure Enable policy is set to On.", + "AdditionalInformation": "Break-glass accounts should be excluded from all Conditional Access policies. Ensure administrators are pre-registered with strong authentication before enforcing the policy.", + "References": "https://learn.microsoft.com/en-us/entra/identity/authentication/concept-authentication-strengths", + "DefaultValue": "MFA strength is not enforced by default" + } + ] + }, + { + "Id": "5.2.2.6", + "Description": "Microsoft Entra ID Protection user risk policies detect the probability that a user account has been compromised. Enable Identity Protection user risk policies.", + "Checks": [ + "entra_identity_protection_user_risk_enabled" + ], + "Attributes": [ + { + "Section": "5 Microsoft Entra admin center", + "SubSection": "5.2.2 Risk-based Conditional Access", + "Profile": "E5 Level 1", + "AssessmentStatus": "Automated", + "Description": "Microsoft Entra ID Protection user risk policies detect the probability that a user account has been compromised. Enable Identity Protection user risk policies.", + "RationaleStatement": "With the user risk policy turned on, Entra ID protection detects the probability that a user account has been compromised. Administrators can configure a user risk conditional access policy to automatically respond to a specific user risk level.", + "ImpactStatement": "Upon policy activation, account access will be either blocked or the user will be required to use multi-factor authentication (MFA) and change their password. Users without registered MFA will be denied access, necessitating an admin to recover the account. To avoid inconvenience, it is advised to configure the MFA registration policy for all users under the User Risk policy. Additionally, users identified in the Risky Users section will be affected by this policy.", + "RemediationProcedure": "To remediate using the UI: 1. Navigate to the Microsoft Entra admin center https://entra.microsoft.com. 2. Click expand ID Protection > Risk-based Conditional Access. 3. Create a new policy by selecting New policy. 4. Set the following conditions within the policy: Under Users choose All users. Under Target resources choose All resources (formerly 'All cloud apps'). Under Conditions choose User risk then Yes and select the user risk level High. Under Grant select Grant access then check Require multifactor authentication or Require authentication strength. Finally check Require password change. Under Session set Sign-in frequency to Every time. Click Select. 5. Under Enable policy set it to Report-only until the organization is ready to enable it. 6. Click Create or Save.", + "AuditProcedure": "To audit using the UI: 1. Navigate to the Microsoft Entra admin center https://entra.microsoft.com. 2. Click expand ID Protection > Risk-based Conditional Access. 3. Ensure that a policy exists with the following criteria and is set to On: Under Users verify All users is included. Ensure that only documented user exclusions exist and that they are reviewed annually. Under Target resources verify All resources (formerly 'All cloud apps') is selected. Under Conditions verify User risk is set to High. Under Grant verify Grant access is selected and either Require multifactor authentication or Require authentication strength are checked. Then verify Require password change is checked. Under Session ensure Sign-in frequency is set to Every time. 4. Ensure Enable policy is set to On.", + "AdditionalInformation": "Break-glass accounts should be excluded from all Conditional Access policies.", + "References": "https://learn.microsoft.com/en-us/entra/id-protection/howto-identity-protection-risk-feedback", + "DefaultValue": "User risk policy is not enabled by default" + } + ] + }, + { + "Id": "5.2.2.7", + "Description": "Microsoft Entra ID Protection sign-in risk detects risks in real-time and offline. A risky sign-in is an indicator for a sign-in attempt that might not have been performed by the legitimate owner of a user account. Enable Identity Protection sign-in risk policies.", + "Checks": [ + "entra_identity_protection_sign_in_risk_enabled" + ], + "Attributes": [ + { + "Section": "5 Microsoft Entra admin center", + "SubSection": "5.2.2 Risk-based Conditional Access", + "Profile": "E5 Level 1", + "AssessmentStatus": "Automated", + "Description": "Microsoft Entra ID Protection sign-in risk detects risks in real-time and offline. A risky sign-in is an indicator for a sign-in attempt that might not have been performed by the legitimate owner of a user account. Enable Identity Protection sign-in risk policies.", + "RationaleStatement": "Turning on the sign-in risk policy ensures that suspicious sign-ins are challenged for multi-factor authentication.", + "ImpactStatement": "When the policy triggers, the user will need MFA to access the account. In the case of a user who hasn't registered MFA on their account, they would be blocked from accessing their account. It is therefore recommended that the MFA registration policy be configured for all users who are a part of the Sign-in Risk policy.", + "RemediationProcedure": "To remediate using the UI: 1. Navigate to the Microsoft Entra admin center https://entra.microsoft.com. 2. Click expand ID Protection > Risk-based Conditional Access. 3. Create a new policy by selecting New policy. 4. Set the following conditions within the policy: Under Users choose All users. Under Target resources choose All resources (formerly 'All cloud apps'). Under Conditions choose Sign-in risk then Yes and check the risk level boxes High and Medium. Under Grant click Grant access then select Require multifactor authentication. Under Session select Sign-in Frequency and set to Every time. Click Select. 5. Under Enable policy set it to Report-only until the organization is ready to enable it. 6. Click Create.", + "AuditProcedure": "To audit using the UI: 1. Navigate to the Microsoft Entra admin center https://entra.microsoft.com. 2. Click expand ID Protection > Risk-based Conditional Access. 3. Ensure that a policy exists with the following criteria and is set to On: Under Users verify All users is included. Ensure that only documented user exclusions exist and that they are reviewed annually. Under Target resources verify All resources (formerly 'All cloud apps') is selected. Under Conditions verify Sign-in risk is set to Yes ensuring High and Medium are selected. Under Grant verify grant Grant access is selected and Require multifactor authentication checked. Under Session verify Sign-in Frequency is set to Every time. 4. Ensure Enable policy is set to On.", + "AdditionalInformation": "Break-glass accounts should be excluded from all Conditional Access policies.", + "References": "https://learn.microsoft.com/en-us/entra/id-protection/concept-identity-protection-risks", + "DefaultValue": "Sign-in risk policy is not enabled by default" + } + ] + }, + { + "Id": "5.2.2.8", + "Description": "Microsoft Entra ID Protection sign-in risk detects risks in real-time and offline. A risky sign-in is an indicator for a sign-in attempt that might not have been performed by the legitimate owner of a user account. Ensure 'sign-in risk' is blocked for medium and high risk.", + "Checks": [], + "Attributes": [ + { + "Section": "5 Microsoft Entra admin center", + "SubSection": "5.2.2 Risk-based Conditional Access", + "Profile": "E5 Level 2", + "AssessmentStatus": "Automated", + "Description": "Microsoft Entra ID Protection sign-in risk detects risks in real-time and offline. A risky sign-in is an indicator for a sign-in attempt that might not have been performed by the legitimate owner of a user account. Ensure 'sign-in risk' is blocked for medium and high risk.", + "RationaleStatement": "Sign-in risk is determined at the time of sign-in and includes criteria across both real-time and offline detections for risk. Blocking sign-in to accounts that have risk can prevent undesired access from potentially compromised devices or unauthorized users.", + "ImpactStatement": "Sign-in risk is heavily dependent on detecting risk based on atypical behaviors. Due to this it is important to run this policy in a report-only mode to better understand how the organization's environment and user activity may influence sign-in risk before turning the policy on. Once it's understood what actions may trigger a medium or high sign-in risk event I.T. can then work to create an environment to reduce false positives.", + "RemediationProcedure": "To remediate using the UI: 1. Navigate to the Microsoft Entra admin center https://entra.microsoft.com. 2. Click expand ID Protection > Risk-based Conditional Access. 3. Create a new policy by selecting New policy. 4. Set the following conditions within the policy: Under Users include All users. Under Target resources include All resources (formerly 'All cloud apps') and do not set any exclusions. Under Conditions choose Sign-in risk values of High and Medium and click Done. Under Grant choose Block access and click Select. 5. Under Enable policy set it to Report-only until the organization is ready to enable it. 6. Click Create.", + "AuditProcedure": "To audit using the UI: 1. Navigate to the Microsoft Entra admin center https://entra.microsoft.com. 2. Click expand ID Protection > Risk-based Conditional Access. 3. Ensure that a policy exists with the following criteria and is set to On: Under Users verify All users is included. Ensure that only documented user exclusions exist and that they are reviewed annually. Under Target resources verify All resources (formerly 'All cloud apps') is selected with no exclusions. Under Conditions verify Sign-in risk values of High and Medium are selected. Under Grant verify Block access is selected. 4. Ensure Enable policy is set to On.", + "AdditionalInformation": "Break-glass accounts should be excluded from all Conditional Access policies.", + "References": "https://learn.microsoft.com/en-us/entra/id-protection/concept-identity-protection-risks#risk-detections-mapped-to-riskeventtype", + "DefaultValue": "Sign-in risk blocking is not enabled by default" + } + ] + }, + { + "Id": "5.2.2.9", + "Description": "Conditional Access (CA) can be configured to enforce access based on the device's compliance status or whether it is Entra hybrid joined. Ensure a managed device is required for authentication.", + "Checks": [ + "entra_managed_device_required_for_authentication" + ], + "Attributes": [ + { + "Section": "5 Microsoft Entra admin center", + "SubSection": "5.2.2 Risk-based Conditional Access", + "Profile": "E3 Level 1", + "AssessmentStatus": "Automated", + "Description": "Conditional Access (CA) can be configured to enforce access based on the device's compliance status or whether it is Entra hybrid joined. Ensure a managed device is required for authentication.", + "RationaleStatement": "Managed devices are considered more secure because they often have additional configuration hardening enforced through centralized management such as Intune or Group Policy. These devices are also typically equipped with MDR/EDR, managed patching and alerting systems. As a result, they provide a safer environment for users to authenticate and operate from. This policy also ensures that attackers must first gain access to a compliant or trusted device before authentication is permitted, reducing the risk posed by compromised account credentials.", + "ImpactStatement": "Unmanaged devices will not be permitted as a valid authenticator. As a result this may require the organization to mature their device enrollment and management. The following devices can be considered managed: Entra hybrid joined from Active Directory, Entra joined and enrolled in Intune with compliance policies, Entra registered and enrolled in Intune with compliance policies. If Guest or external users are collaborating with the organization, they must either be excluded or onboarded with a compliant device to authenticate.", + "RemediationProcedure": "To remediate using the UI: 1. Navigate to the Microsoft Entra admin center https://entra.microsoft.com. 2. Click expand ID Protection > Risk-based Conditional Access. 3. Create a new policy by selecting New policy. Under Users include All users. Under Target resources include All resources (formerly 'All cloud apps'). Under Grant select Grant access. Select only the checkboxes Require device to be marked as compliant and Require Microsoft Entra hybrid joined device. Choose Require one of the selected controls and click Select at the bottom. 4. Under Enable policy set it to Report-only until the organization is ready to enable it. 5. Click Create.", + "AuditProcedure": "To audit using the UI: 1. Navigate to the Microsoft Entra admin center https://entra.microsoft.com. 2. Click expand ID Protection > Risk-based Conditional Access. 3. Ensure that a policy exists with the following criteria and is set to On: Under Users verify All users is included. Ensure that only documented user exclusions exist and that they are reviewed annually. Under Target resources verify All resources (formerly 'All cloud apps') is selected. Ensure that only documented resource exclusions exist and that they are reviewed annually. Under Grant verify that only Require device to be marked as compliant and Require Microsoft Entra hybrid joined device are checked. Under Grant verify Require one of the selected controls is selected. 4. Ensure Enable policy is set to On.", + "AdditionalInformation": "Break-glass accounts should be excluded from all Conditional Access policies. Guest user accounts, if collaborating with the organization, should be considered when testing this policy.", + "References": "https://learn.microsoft.com/en-us/entra/identity/conditional-access/concept-conditional-access-grant#require-device-to-be-marked-as-compliant", + "DefaultValue": "Managed device requirement is not enforced by default" + } + ] + }, + { + "Id": "5.2.2.10", + "Description": "Conditional Access (CA) can be configured to enforce access based on the device's compliance status or whether it is Entra hybrid joined. Ensure a managed device is required to register security information.", + "Checks": [ + "entra_managed_device_required_for_mfa_registration" + ], + "Attributes": [ + { + "Section": "5 Microsoft Entra admin center", + "SubSection": "5.2.2 Risk-based Conditional Access", + "Profile": "E3 Level 1", + "AssessmentStatus": "Automated", + "Description": "Conditional Access (CA) can be configured to enforce access based on the device's compliance status or whether it is Entra hybrid joined. Ensure a managed device is required to register security information.", + "RationaleStatement": "Requiring registration on a managed device significantly reduces the risk of bad actors using stolen credentials to register security information. Accounts that are created but never registered with an MFA method are particularly vulnerable to this type of attack. Enforcing this requirement will both reduce the attack surface for fake registrations and ensure that legitimate users register using trusted devices which typically have additional security measures in place already.", + "ImpactStatement": "The organization will be required to have a mature device management process. New devices provided to users will need to be pre-enrolled in Intune, auto-enrolled or be Entra hybrid joined. Otherwise, the user will be unable to complete registration, requiring additional resources from I.T. This could be more disruptive in remote worker environments where the MDM maturity is low.", + "RemediationProcedure": "To remediate using the UI: 1. Navigate to the Microsoft Entra admin center https://entra.microsoft.com. 2. Click expand ID Protection > Risk-based Conditional Access. 3. Create a new policy by selecting New policy. Under Users include All users. Under Target resources select User actions and check Register security information. Under Grant select Grant access. Check only Require multifactor authentication and Require Microsoft Entra hybrid joined device. Choose Require one of the selected controls and click Select at the bottom. 4. Under Enable policy set it to Report-only until the organization is ready to enable it. 5. Click Create.", + "AuditProcedure": "To audit using the UI: 1. Navigate to the Microsoft Entra admin center https://entra.microsoft.com. 2. Click expand ID Protection > Risk-based Conditional Access. 3. Ensure that a policy exists with the following criteria and is set to On: Under Users verify All users is included. Ensure that only documented user exclusions exist and that they are reviewed annually. Under Target resources verify User actions is selected with Register security information checked. Under Grant verify that only Require device to be marked as compliant and Require Microsoft Entra hybrid joined device are checked. Under Grant verify Require one of the selected controls is selected. 4. Ensure Enable policy is set to On.", + "AdditionalInformation": "Break-glass accounts should be excluded from all Conditional Access policies.", + "References": "https://learn.microsoft.com/en-us/entra/identity/conditional-access/concept-conditional-access-cloud-apps#user-actions", + "DefaultValue": "Managed device requirement for MFA registration is not enforced by default" + } + ] + }, + { + "Id": "5.2.2.11", + "Description": "Sign-in frequency defines the time period before a user is asked to sign in again when attempting to access a resource. Ensure sign-in frequency for Intune Enrollment is set to 'Every time'.", + "Checks": [ + "entra_admin_users_sign_in_frequency_enabled" + ], + "Attributes": [ + { + "Section": "5 Microsoft Entra admin center", + "SubSection": "5.2.2 Risk-based Conditional Access", + "Profile": "E3 Level 1", + "AssessmentStatus": "Automated", + "Description": "Sign-in frequency defines the time period before a user is asked to sign in again when attempting to access a resource. Ensure sign-in frequency for Intune Enrollment is set to 'Every time'.", + "RationaleStatement": "Intune Enrollment is considered a sensitive action and should be safeguarded. An attack path exists that allows for a bypass of device compliance Conditional Access rule. This could allow compromised credentials to be used through a newly registered device enrolled in Intune, enabling persistence and privilege escalation. Setting sign-in frequency to every time limits the timespan an attacker could use fresh credentials to enroll a new device to Intune.", + "ImpactStatement": "New users enrolling into Intune through an automated process may need to sign-in again if the enrollment process goes on for too long.", + "RemediationProcedure": "To remediate using the UI: 1. Navigate to the Microsoft Entra admin center https://entra.microsoft.com. 2. Click expand ID Protection > Risk-based Conditional Access. 3. Create a new policy by selecting New policy. Under Users include All users. Under Target resources select Resources (formerly cloud apps), choose Select resources and add Microsoft Intune Enrollment to the list. Under Grant select Grant access. Check either Require multifactor authentication or Require authentication strength. Under Session check Sign-in frequency and select Every time. 4. Under Enable policy set it to Report-only until the organization is ready to enable it. 5. Click Create.", + "AuditProcedure": "To audit using the UI: 1. Navigate to the Microsoft Entra admin center https://entra.microsoft.com. 2. Click expand ID Protection > Risk-based Conditional Access. 3. Ensure that a policy exists with the following criteria and is set to On: Under Users verify All users is included. Ensure that only documented user exclusions exist and that they are reviewed annually. Under Target resources verify Resources (formerly cloud apps) includes Microsoft Intune Enrollment. Under Grant verify Require multifactor authentication or Require authentication strength is checked. Under Session verify Sign-in frequency is set to Every time. 4. Ensure Enable policy is set to On.", + "AdditionalInformation": "Break-glass accounts should be excluded from all Conditional Access policies. If the Microsoft Intune Enrollment cloud app isn't available then it must be created. To add the app for new tenants, a Microsoft Entra administrator must create a service principal object, with app ID d4ebce55-015a-49b5-a083-c84d1797ae8c, in PowerShell or Microsoft Graph.", + "References": "https://learn.microsoft.com/en-us/entra/identity/conditional-access/concept-session-lifetime#require-reauthentication-every-time", + "DefaultValue": "Sign-in frequency defaults to 90 days" + } + ] + }, + { + "Id": "5.2.2.12", + "Description": "The Microsoft identity platform supports the device authorization grant, which allows users to sign in to input-constrained devices such as a smart TV, IoT device, or a printer. Ensure the device code sign-in flow is blocked.", + "Checks": [], + "Attributes": [ + { + "Section": "5 Microsoft Entra admin center", + "SubSection": "5.2.2 Risk-based Conditional Access", + "Profile": "E3 Level 1", + "AssessmentStatus": "Automated", + "Description": "The Microsoft identity platform supports the device authorization grant, which allows users to sign in to input-constrained devices such as a smart TV, IoT device, or a printer. Ensure the device code sign-in flow is blocked.", + "RationaleStatement": "Since August 2024, Microsoft has observed threat actors, such as Storm-2372, employing 'device code phishing' attacks. These attacks deceive users into logging into productivity applications, capturing authentication tokens to gain further access to compromised accounts. To mitigate this specific attack, block authentication code flows and permit only those from devices within trusted environments, identified by specific IP addresses.", + "ImpactStatement": "Some administrative overhead will be required for stricter management of these devices. Since exclusions do not violate compliance, this feature can still be utilized effectively within a controlled environment.", + "RemediationProcedure": "To remediate using the UI: 1. Navigate to the Microsoft Entra admin center https://entra.microsoft.com. 2. Click expand ID Protection > Risk-based Conditional Access. 3. Create a new policy by selecting New policy. Under Users include All users. Under Target resources include All resources (formerly 'All cloud apps'). Under Conditions select Authentication flows and check Device code flow. Under Grant select Block access. Click Select. 4. Under Enable policy set it to Report-only until the organization is ready to enable it. 5. Click Create.", + "AuditProcedure": "To audit using the UI: 1. Navigate to the Microsoft Entra admin center https://entra.microsoft.com. 2. Click expand ID Protection > Risk-based Conditional Access. 3. Ensure that a policy exists with the following criteria and is set to On: Under Users verify All users is included. Ensure that only documented user exclusions exist and that they are reviewed annually. Under Target resources verify All resources (formerly 'All cloud apps') is selected. Ensure that only documented resource exclusions exist and that they are reviewed annually. Under Conditions select Authentication flows and verify Device code flow is checked. Under Grant verify Block access is selected. 4. Ensure Enable policy is set to On.", + "AdditionalInformation": "Break-glass accounts should be excluded from all Conditional Access policies.", + "References": "https://learn.microsoft.com/en-us/entra/identity/conditional-access/concept-authentication-flows", + "DefaultValue": "Device code flow is not blocked by default" + } + ] + }, + { + "Id": "5.2.3.1", + "Description": "Microsoft provides supporting settings to enhance the configuration of the Microsoft Authenticator application. These settings provide users with additional information and context when they receive MFA passwordless and push requests. Ensure Microsoft Authenticator is configured to protect against MFA fatigue.", + "Checks": [ + "entra_intune_enrollment_sign_in_frequency_every_time" + ], + "Attributes": [ + { + "Section": "5 Microsoft Entra admin center", + "SubSection": "5.2.3 Authentication Methods", + "Profile": "E3 Level 1", + "AssessmentStatus": "Automated", + "Description": "Microsoft provides supporting settings to enhance the configuration of the Microsoft Authenticator application. These settings provide users with additional information and context when they receive MFA passwordless and push requests. Ensure Microsoft Authenticator is configured to protect against MFA fatigue.", + "RationaleStatement": "As the use of strong authentication has become more widespread, attackers have started to exploit the tendency of users to experience 'MFA fatigue.' This occurs when users are repeatedly asked to provide additional forms of identification, leading them to eventually approve requests without fully verifying the source. To counteract this, number matching can be employed to ensure the security of the authentication process. With this method, users are prompted to confirm a number displayed on their original device and enter it into the device being used for MFA.", + "ImpactStatement": "Additional interaction will be required by end users using number matching as opposed to simply pressing 'Approve' for login attempts.", + "RemediationProcedure": "To remediate using the UI: 1. Navigate to the Microsoft Entra admin center https://entra.microsoft.com. 2. Click to expand Entra ID > Authentication methods select Policies. 3. Select Microsoft Authenticator. 4. Under Enable and Target ensure the setting is set to Enable. 5. Select Configure. 6. Set the following Microsoft Authenticator settings: Require number matching for push notifications Status is set to Enabled, Target All users. Show application name in push and passwordless notifications is set to Enabled, Target All users. Show geographic location in push and passwordless notifications is set to Enabled, Target All users.", + "AuditProcedure": "To audit using the UI: 1. Navigate to the Microsoft Entra admin center https://entra.microsoft.com. 2. Click to expand Entra ID > Authentication methods select Policies. 3. Under Method select Microsoft Authenticator. 4. Under Enable and Target verify the setting is set to Enable. 5. In the Include tab ensure All users is selected. 6. In the Exclude tab ensure only valid groups are present (i.e. Break Glass accounts). 7. Select Configure. 8. Verify the following Microsoft Authenticator settings: Require number matching for push notifications Status is set to Enabled, Target All users. Show application name in push and passwordless notifications is set to Enabled, Target All users. Show geographic location in push and passwordless notifications is set to Enabled, Target All users.", + "AdditionalInformation": "On February 27, 2023 Microsoft started enforcing number matching tenant-wide for all users using Microsoft Authenticator.", + "References": "https://learn.microsoft.com/en-us/entra/identity/authentication/how-to-mfa-number-match", + "DefaultValue": "Microsoft-managed" + } + ] + }, + { + "Id": "5.2.3.2", + "Description": "With Entra Password Protection, default global banned password lists are automatically applied to all users in an Entra ID tenant. To support business and security needs, custom banned password lists can be defined. Ensure custom banned passwords lists are used.", + "Checks": [], + "Attributes": [ + { + "Section": "5 Microsoft Entra admin center", + "SubSection": "5.2.3 Authentication Methods", + "Profile": "E3 Level 1", + "AssessmentStatus": "Automated", + "Description": "With Entra Password Protection, default global banned password lists are automatically applied to all users in an Entra ID tenant. To support business and security needs, custom banned password lists can be defined. Ensure custom banned passwords lists are used.", + "RationaleStatement": "Creating a new password can be difficult regardless of one's technical background. It is common to look around one's environment for suggestions when building a password, however, this may include picking words specific to the organization as inspiration for a password. An adversary may employ what is called a 'mangler' to create permutations of these specific words in an attempt to crack passwords or hashes making it easier to reach their goal.", + "ImpactStatement": "If a custom banned password list includes too many common dictionary words, or short words that are part of compound words, then perfectly secure passwords may be blocked. The organization should consider a balance between security and usability when creating a list.", + "RemediationProcedure": "To remediate using the UI: 1. Navigate to Microsoft Entra admin center https://entra.microsoft.com/. 2. Click to expand Entra ID > Authentication methods. 3. Select Password protection. 4. Set Enforce custom list to Yes. 5. In Custom banned password list create a list using suggestions such as: Brand names, Product names, Locations such as company headquarters, Company-specific internal terms, Abbreviations that have specific company meaning. 6. Click Save.", + "AuditProcedure": "To audit using the UI: 1. Navigate to Microsoft Entra admin center https://entra.microsoft.com/. 2. Click to expand Entra ID > Authentication methods. 3. Select Password protection. 4. Verify Enforce custom list is set to Yes. 5. Verify Custom banned password list contains entries specific to the organization or matches a pre-determined list.", + "AdditionalInformation": "", + "References": "https://learn.microsoft.com/en-us/entra/identity/authentication/concept-password-ban-bad#custom-banned-password-list", + "DefaultValue": "Enforce custom list is disabled by default" + } + ] + }, + { + "Id": "5.2.3.3", + "Description": "Microsoft Entra Password Protection provides a global and custom banned password list. A password change request fails if there's a match in these banned password list. To protect on-premises Active Directory Domain Services (AD DS) environment, install and configure Entra Password Protection.", + "Checks": [], + "Attributes": [ + { + "Section": "5 Microsoft Entra admin center", + "SubSection": "5.2.3 Authentication Methods", + "Profile": "E3 Level 1", + "AssessmentStatus": "Automated", + "Description": "Microsoft Entra Password Protection provides a global and custom banned password list. A password change request fails if there's a match in these banned password list. To protect on-premises Active Directory Domain Services (AD DS) environment, install and configure Entra Password Protection.", + "RationaleStatement": "This feature protects an organization by prohibiting the use of weak or leaked passwords. In addition, organizations can create custom banned password lists to prevent their users from using easily guessed passwords that are specific to their industry. Deploying this feature to Active Directory will strengthen the passwords that are used in the environment.", + "ImpactStatement": "The potential impact associated with implementation of this setting is dependent upon the existing password policies in place in the environment. For environments that have strong password policies in place, the impact will be minimal. For organizations that do not have strong password policies in place, implementation of Microsoft Entra Password Protection may require users to change passwords and adhere to more stringent requirements than they have been accustomed to.", + "RemediationProcedure": "To remediate: Download and install the Azure AD Password Proxies and DC Agents from https://www.microsoft.com/download/details.aspx?id=57071. Then: 1. Navigate to Microsoft Entra admin center https://entra.microsoft.com/. 2. Click to expand Protection select Authentication methods. 3. Select Password protection and set Enable password protection on Windows Server Active Directory to Yes and Mode to Enforced.", + "AuditProcedure": "To audit using the UI: 1. Navigate to Microsoft Entra admin center https://entra.microsoft.com/. 2. Click to expand Entra ID > Authentication methods. 3. Select Password protection and ensure that Enable password protection on Windows Server Active Directory is set to Yes and that Mode is set to Enforced.", + "AdditionalInformation": "This recommendation applies to Hybrid deployments only and will have no impact unless working with on-premises Active Directory.", + "References": "https://learn.microsoft.com/en-us/entra/identity/authentication/howto-password-ban-bad-on-premises-operations", + "DefaultValue": "Enable - Yes, Mode - Audit" + } + ] + }, + { + "Id": "5.2.3.4", + "Description": "Microsoft defines Multifactor authentication capable as being registered and enabled for a strong authentication method. The method must also be allowed by the authentication methods policy. Ensure all member users are MFA capable.", + "Checks": [ + "entra_users_mfa_capable" + ], + "Attributes": [ + { + "Section": "5 Microsoft Entra admin center", + "SubSection": "5.2.3 Authentication Methods", + "Profile": "E3 Level 1", + "AssessmentStatus": "Automated", + "Description": "Microsoft defines Multifactor authentication capable as being registered and enabled for a strong authentication method. The method must also be allowed by the authentication methods policy. Ensure all member users are MFA capable.", + "RationaleStatement": "Multifactor authentication requires an individual to present a minimum of two separate forms of authentication before access is granted. Users who are not MFA Capable have never registered a strong authentication method for multifactor authentication that is within policy and may not be using MFA. This could be a result of having never signed in, exclusion from a Conditional Access (CA) policy requiring MFA, or a CA policy does not exist. Reviewing this list of users will help identify possible lapses in policy or procedure.", + "ImpactStatement": "When using the UI audit method guest users will appear in the report and unless the organization is applying MFA rules to guests then they will need to be manually filtered. Accounts that provide on-premises directory synchronization also appear in these reports.", + "RemediationProcedure": "Remediation steps will depend on the status of the personnel in question or configuration of Conditional Access policies. Administrators should review each user identified on a case-by-case basis. For users who have never signed on, employment status should be reviewed and appropriate action taken. For Conditional Access policy applicability: Ensure a CA policy is in place requiring all users to use MFA. Ensure the user is not excluded from the CA MFA policy. Ensure the policy's state is set to On. Use What if to determine applicable CA policies.", + "AuditProcedure": "To audit using the UI: 1. Navigate to Microsoft Entra admin center https://entra.microsoft.com/. 2. Click to expand Entra ID > Authentication methods. 3. Select User registration details. 4. Set the filter option Multifactor authentication capable to Not Capable. 5. Review the non-guest users in this list. 6. Excluding any exceptions users found in this report may require remediation.", + "AdditionalInformation": "The CA rule must be in place for a successful deployment of Multifactor Authentication. This policy is outlined in the conditional access section 5.2.2. Possible exceptions include on-premises synchronization accounts.", + "References": "https://learn.microsoft.com/en-us/entra/identity/authentication/howto-authentication-methods-activity", + "DefaultValue": "Users are not MFA capable by default until they register" + } + ] + }, + { + "Id": "5.2.3.5", + "Description": "Authentication methods support a wide variety of scenarios for signing in to Microsoft 365 resources. Some of these methods are inherently more secure than others. Ensure weak authentication methods (SMS and Voice Call) are disabled.", + "Checks": [], + "Attributes": [ + { + "Section": "5 Microsoft Entra admin center", + "SubSection": "5.2.3 Authentication Methods", + "Profile": "E3 Level 1", + "AssessmentStatus": "Automated", + "Description": "Authentication methods support a wide variety of scenarios for signing in to Microsoft 365 resources. Some of these methods are inherently more secure than others. Ensure weak authentication methods (SMS and Voice Call) are disabled.", + "RationaleStatement": "Traditional MFA methods such as SMS codes, email-based OTPs, and push notifications are becoming less effective against today's attackers. Sophisticated phishing campaigns have demonstrated that second factors can be intercepted or spoofed. Attackers now exploit social engineering, man-in-the-middle tactics, and user fatigue (e.g., MFA bombing) to bypass these mechanisms. The SMS and Voice call methods are vulnerable to SIM swapping which could allow an attacker to gain access to your Microsoft 365 account.", + "ImpactStatement": "There may be increased administrative overhead in adopting more secure authentication methods depending on the maturity of the organization.", + "RemediationProcedure": "To remediate using the UI: 1. Navigate to Microsoft Entra admin center https://entra.microsoft.com/. 2. Click to expand Entra ID > Authentication methods. 3. Select Policies. 4. Inspect each method that is out of compliance and remediate: Click on the method to open it. Change the Enable toggle to the off position. Click Save.", + "AuditProcedure": "To audit using the UI: 1. Navigate to Microsoft Entra admin center https://entra.microsoft.com/. 2. Click to expand Entra ID > Authentication methods. 3. Select Policies. 4. Verify that the following methods in the Enabled column are set to No: Method: SMS, Method: Voice call.", + "AdditionalInformation": "", + "References": "https://learn.microsoft.com/en-us/entra/identity/authentication/concept-authentication-methods-manage", + "DefaultValue": "SMS: Disabled, Voice Call: Disabled" + } + ] + }, + { + "Id": "5.2.3.6", + "Description": "System-preferred multifactor authentication (MFA) prompts users to sign in by using the most secure method they registered. Ensure system-preferred multifactor authentication is enabled.", + "Checks": [], + "Attributes": [ + { + "Section": "5 Microsoft Entra admin center", + "SubSection": "5.2.3 Authentication Methods", + "Profile": "E3 Level 1", + "AssessmentStatus": "Automated", + "Description": "System-preferred multifactor authentication (MFA) prompts users to sign in by using the most secure method they registered. Ensure system-preferred multifactor authentication is enabled.", + "RationaleStatement": "Regardless of the authentication method enabled by an administrator or set as preferred by the user, the system will dynamically select the most secure option available at the time of authentication. This approach acts as an additional safeguard to prevent the use of weaker methods, such as voice calls, SMS, and email OTPs, which may have been inadvertently left enabled due to misconfiguration or lack of configuration hardening. Enforcing the default behavior also ensures the feature is not disabled.", + "ImpactStatement": "The Microsoft managed value of system-preferred MFA is Enabled and as such enforces the default behavior. No additional impact is expected.", + "RemediationProcedure": "To remediate using the UI: 1. Navigate to Microsoft Entra admin center https://entra.microsoft.com/. 2. Click to expand Entra ID > Authentication methods. 3. Select Settings. 4. Set the System-preferred multifactor authentication State to Enabled and include All users. 5. Any users exclusions should be documented and reviewed annually.", + "AuditProcedure": "To audit using the UI: 1. Navigate to Microsoft Entra admin center https://entra.microsoft.com/. 2. Click to expand Entra ID > Authentication methods. 3. Select Settings. 4. Verify the System-preferred multifactor authentication State is set to Enabled and All users are included. 5. Ensure that only documented exclusions exist and that they are reviewed annually.", + "AdditionalInformation": "Due to known issues with certificate-based authentication (CBA) and system-preferred MFA, Microsoft moved CBA to the bottom of the list. It is still considered a strong authentication method.", + "References": "https://learn.microsoft.com/en-us/entra/identity/authentication/concept-system-preferred-multifactor-authentication", + "DefaultValue": "Microsoft Managed (Enabled)" + } + ] + }, + { + "Id": "5.2.3.7", + "Description": "The email one-time passcode feature is a way to authenticate B2B collaboration users when they can't be authenticated through other means. Ensure the email OTP authentication method is disabled.", + "Checks": [], + "Attributes": [ + { + "Section": "5 Microsoft Entra admin center", + "SubSection": "5.2.3 Authentication Methods", + "Profile": "E3 Level 2", + "AssessmentStatus": "Automated", + "Description": "The email one-time passcode feature is a way to authenticate B2B collaboration users when they can't be authenticated through other means. Ensure the email OTP authentication method is disabled.", + "RationaleStatement": "Traditional MFA methods such as SMS codes, email-based OTPs, and push notifications are becoming less effective against today's attackers. Sophisticated phishing campaigns have demonstrated that second factors can be intercepted or spoofed. Attackers now exploit social engineering, man-in-the-middle tactics, and user fatigue (e.g., MFA bombing) to bypass these mechanisms.", + "ImpactStatement": "Disabling Email OTP will prevent one-time pass codes from being sent to unverified guest users accessing Microsoft 365 resources on the tenant. They will be required to use a personal Microsoft account, a managed Microsoft Entra account, be part of a federation or be configured as a guest in the host tenant's Microsoft Entra ID.", + "RemediationProcedure": "To remediate using the UI: 1. Navigate to Microsoft Entra admin center https://entra.microsoft.com/. 2. Click to expand Entra ID > Authentication methods. 3. Select Policies. 4. Click on Email OTP. 5. Change the Enable toggle to the off position. 6. Click Save.", + "AuditProcedure": "To audit using the UI: 1. Navigate to Microsoft Entra admin center https://entra.microsoft.com/. 2. Click to expand Entra ID > Authentication methods. 3. Select Policies. 4. Verify that Email OTP is set to No in the Enabled column.", + "AdditionalInformation": "", + "References": "https://learn.microsoft.com/en-us/entra/external-id/one-time-passcode", + "DefaultValue": "Email OTP: Enabled" + } + ] + }, + { + "Id": "5.2.4.1", + "Description": "Enabling self-service password reset allows users to reset their own passwords in Entra ID. When users sign in to Microsoft 365, they will be prompted to enter additional contact information that will help them reset their password in the future. Ensure 'Self service password reset enabled' is set to 'All'.", + "Checks": [], + "Attributes": [ + { + "Section": "5 Microsoft Entra admin center", + "SubSection": "5.2.4 Password reset", + "Profile": "E3 Level 1", + "AssessmentStatus": "Manual", + "Description": "Enabling self-service password reset allows users to reset their own passwords in Entra ID. When users sign in to Microsoft 365, they will be prompted to enter additional contact information that will help them reset their password in the future. Ensure 'Self service password reset enabled' is set to 'All'.", + "RationaleStatement": "Enabling Self-Service Password Reset (SSPR) significantly reduces helpdesk interactions, streamlining support operations and improving user experience. Traditional methods involving temporary passwords pose notable security risks—they are often weak, predictable, and susceptible to interception. This creates a window of opportunity for threat actors to compromise accounts before users can update their credentials. SSPR minimizes credential exposure and strengthens overall identity protection.", + "ImpactStatement": "Users will be required to provide additional contact information to enroll in self-service password reset. Additionally, minor user education may be required for users that are used to calling a help desk for assistance with password resets. This is unavailable if using Entra Connect / Sync in a hybrid environment.", + "RemediationProcedure": "To remediate using the UI: 1. Navigate to Microsoft Entra admin center https://entra.microsoft.com/. 2. Click to expand Entra ID > Password reset select Properties. 3. Set Self service password reset enabled to All.", + "AuditProcedure": "To audit using the UI: 1. Navigate to Microsoft Entra admin center https://entra.microsoft.com/. 2. Click to expand Entra ID > Password reset select Properties. 3. Ensure Self service password reset enabled is set to All.", + "AdditionalInformation": "Effective Oct. 1st, 2022, Microsoft will begin to enable combined registration for all users in Entra ID tenants created before August 15th, 2020. Tenants created after this date are enabled with combined registration by default.", + "References": "https://learn.microsoft.com/en-us/entra/identity/authentication/tutorial-enable-sspr", + "DefaultValue": "Self service password reset is not enabled by default" + } + ] + }, + { + "Id": "5.3.1", + "Description": "Microsoft Entra Privileged Identity Management can be used to audit roles, allow just in time activation of roles and allow for periodic role attestation. Organizations should remove permanent members from privileged Office 365 roles and instead make them eligible, through a JIT activation workflow. Ensure 'Privileged Identity Management' is used to manage roles.", + "Checks": [], + "Attributes": [ + { + "Section": "5 Microsoft Entra admin center", + "SubSection": "5.3 ID Governance", + "Profile": "E5 Level 2", + "AssessmentStatus": "Automated", + "Description": "Microsoft Entra Privileged Identity Management can be used to audit roles, allow just in time activation of roles and allow for periodic role attestation. Organizations should remove permanent members from privileged Office 365 roles and instead make them eligible, through a JIT activation workflow. Ensure 'Privileged Identity Management' is used to manage roles.", + "RationaleStatement": "Organizations want to minimize the number of people who have access to secure information or resources, because that reduces the chance of a malicious actor getting that access, or an authorized user inadvertently impacting a sensitive resource. However, users still need to carry out privileged operations in Entra ID. Organizations can give users just-in-time (JIT) privileged access to roles. There is a need for oversight for what those users are doing with their administrator privileges. PIM helps to mitigate the risk of excessive, unnecessary, or misused access rights.", + "ImpactStatement": "The implementation of Just in Time privileged access is likely to necessitate changes to administrator routine. Administrators will only be granted access to administrative roles when required. When administrators request role activation, they will need to document the reason for requiring role access, anticipated time required to have the access, and to reauthenticate to enable role access.", + "RemediationProcedure": "To remediate using the UI: 1. Navigate to Microsoft Entra admin center https://entra.microsoft.com/. 2. Click to expand Identity Governance select Privileged Identity Management. 3. Under Manage select Microsoft Entra Roles. 4. Under Manage select Roles. 5. Inspect the sensitive roles. For each of the members that have an ASSIGNMENT TYPE of Permanent, click on the ... and choose Make eligible.", + "AuditProcedure": "To audit using the UI: 1. Navigate to Microsoft Entra admin center https://entra.microsoft.com/. 2. Click to expand Identity Governance select Privileged Identity Management. 3. Under Manage select Microsoft Entra Roles. 4. Under Manage select Roles. 5. Inspect at a minimum the following sensitive roles to ensure the members are Eligible and not Permanent: Application Administrator, Authentication Administrator, Billing Administrator, Cloud Application Administrator, Conditional Access Administrator, Exchange Administrator, Global Administrator, Global Reader, Helpdesk Administrator, Password Administrator, Privileged Authentication Administrator, Privileged Role Administrator, Security Administrator, SharePoint Administrator, User Administrator.", + "AdditionalInformation": "If all global admins become eligible then there will be no global admin to receive notifications, by default. Alerts are sent to TenantAdmins, including Global Administrators, by default. To ensure proper receipt, configure alerts to be sent to security or operations staff with valid email addresses or a security operations center.", + "References": "https://learn.microsoft.com/en-us/entra/id-governance/privileged-identity-management/pim-configure", + "DefaultValue": "PIM is not configured by default" + } + ] + }, + { + "Id": "5.3.2", + "Description": "Access reviews enable administrators to establish an efficient automated process for reviewing group memberships, access to enterprise applications, and role assignments. These reviews can be scheduled to recur regularly. Ensure 'Access reviews' for Guest Users are configured to be performed no less frequently than monthly.", + "Checks": [], + "Attributes": [ + { + "Section": "5 Microsoft Entra admin center", + "SubSection": "5.3 ID Governance", + "Profile": "E5 Level 1", + "AssessmentStatus": "Automated", + "Description": "Access reviews enable administrators to establish an efficient automated process for reviewing group memberships, access to enterprise applications, and role assignments. These reviews can be scheduled to recur regularly. Ensure 'Access reviews' for Guest Users are configured to be performed no less frequently than monthly.", + "RationaleStatement": "Access to groups and applications for guests can change over time. If a guest user's access to a particular folder goes unnoticed, they may unintentionally gain access to sensitive data if a member adds new files or data to the folder or application. Access reviews can help reduce the risks associated with outdated assignments by requiring a member of the organization to conduct the reviews. Furthermore, these reviews can enable a fail-closed mechanism to remove access to the subject if the reviewer does not respond to the review.", + "ImpactStatement": "Access reviews that are ignored may cause guest users to lose access to resources temporarily.", + "RemediationProcedure": "To remediate using the UI: 1. Navigate to Microsoft Entra admin center https://entra.microsoft.com/. 2. Click to expand Identity Governance and select Access reviews. 3. Click New access review. 4. Select what to review choose Teams + Groups. 5. Review Scope set to All Microsoft 365 groups with guest users, do not exclude groups. 6. Scope set to Guest users only then click Next: Reviews. 7. Select reviewers an appropriate user that is NOT the guest user themselves. 8. Duration (in days) at most 3. 9. Review recurrence is Monthly or more frequent. 10. End is set to Never, then click Next: Settings. 11. Check Auto apply results to resource. 12. Set If reviewers don't respond to Remove access. 13. Check the following: Justification required, E-mail notifications, Reminders. 14. Click Next: Review + Create and finally click Create.", + "AuditProcedure": "To audit using the UI: 1. Navigate to Microsoft Entra admin center https://entra.microsoft.com/. 2. Click to expand Identity Governance and select Access reviews. 3. Inspect the access reviews, and ensure an access review is created with the following criteria: Overview: Scope is set to Guest users only and status is Active. Reviewers: Ensure appropriate reviewer(s) are designated. Settings > General: Mail notifications and Reminders are set to Enable. Reviewers: Require reason on approval is set to Enable. Scheduling: Frequency is Monthly or more frequent. When completed: Auto apply results to resource is set to Enable. When completed: If reviewers don't respond is set to Remove access.", + "AdditionalInformation": "", + "References": "https://learn.microsoft.com/en-us/entra/id-governance/access-reviews-overview", + "DefaultValue": "By default access reviews are not configured" + } + ] + }, + { + "Id": "5.3.3", + "Description": "Access reviews enable administrators to establish an efficient automated process for reviewing group memberships, access to enterprise applications, and role assignments. Ensure 'Access reviews' for privileged roles are configured to be done monthly or more frequently.", + "Checks": [], + "Attributes": [ + { + "Section": "5 Microsoft Entra admin center", + "SubSection": "5.3 ID Governance", + "Profile": "E5 Level 1", + "AssessmentStatus": "Automated", + "Description": "Access reviews enable administrators to establish an efficient automated process for reviewing group memberships, access to enterprise applications, and role assignments. Ensure 'Access reviews' for privileged roles are configured to be done monthly or more frequently.", + "RationaleStatement": "Regular review of critical high privileged roles in Entra ID will help identify role drift, or potential malicious activity. This will enable the practice and application of 'separation of duties' where even non-privileged users like security auditors can be assigned to review assigned roles in an organization. Furthermore, if configured these reviews can enable a fail-closed mechanism to remove access to the subject if the reviewer does not respond to the review.", + "ImpactStatement": "In order to avoid disruption reviewers who have the authority to revoke roles should be trusted individuals who understand the significance of access reviews. Additionally, the principle of separation of duties should be applied to ensure that no administrator is responsible for reviewing their own access levels. If the reviews are configured to automatically revoke highly privileged roles like the Global Administrator role, then this could result in removing all Global Administrators from the organization.", + "RemediationProcedure": "To remediate using the UI: 1. Navigate to Microsoft Entra admin center https://entra.microsoft.com/. 2. Click to expand Identity Governance and select Privileged Identity Management. 3. Select Microsoft Entra Roles under Manage. 4. Select Access reviews and click New access review. Provide a name and description. Set Frequency to Monthly or more frequently. Set Duration (in days) to at most 14. Set End to Never. Set Users scope to All users and groups. In Role select these roles: Global Administrator, Exchange Administrator, SharePoint Administrator, Teams Administrator, Security Administrator. Set Assignment type to All active and eligible assignments. Set Reviewers member(s) responsible for this type of review, other than self. 5. Upon completion settings: Set Auto apply results to resource to Enable. Set If reviewers don't respond to No change. 6. Advanced settings: Set Show recommendations to Enable. Set Require reason on approval to Enable. Set Mail notifications to Enable. Set Reminders to Enable. 7. Click Start to save the review.", + "AuditProcedure": "To audit using the UI: 1. Navigate to Microsoft Entra admin center https://entra.microsoft.com/. 2. Click to expand Identity Governance and select Privileged Identity Management. 3. Select Microsoft Entra Roles under Manage. 4. Select Access reviews. 5. Ensure there are access reviews configured for each high privileged roles and each meets the criteria: Scope - Everyone, Status - Active, Mail notifications - Enable, Reminders - Enable, Require reason on approval - Enable, Frequency - Monthly or more frequently, Duration (in days) - 14 at most, Auto apply results to resource - Enable, If reviewers don't respond - No change.", + "AdditionalInformation": "Care should be taken when configuring the If reviewers don't respond setting for Global Administrator reviews, if misconfigured break-glass accounts could automatically have roles revoked.", + "References": "https://learn.microsoft.com/en-us/entra/id-governance/privileged-identity-management/pim-create-roles-and-resource-roles-review", + "DefaultValue": "By default access reviews are not configured" + } + ] + }, + { + "Id": "5.3.4", + "Description": "Microsoft Entra Privileged Identity Management can be used to audit roles, allow just in time activation of roles and allow for periodic role attestation. Requiring approval before activation allows one of the selected approvers to first review and then approve the activation prior to PIM granted the role. Ensure approval is required for Global Administrator role activation.", + "Checks": [], + "Attributes": [ + { + "Section": "5 Microsoft Entra admin center", + "SubSection": "5.3 ID Governance", + "Profile": "E5 Level 1", + "AssessmentStatus": "Automated", + "Description": "Microsoft Entra Privileged Identity Management can be used to audit roles, allow just in time activation of roles and allow for periodic role attestation. Requiring approval before activation allows one of the selected approvers to first review and then approve the activation prior to PIM granted the role. Ensure approval is required for Global Administrator role activation.", + "RationaleStatement": "Requiring approval for Global Administrator role activation enhances visibility and accountability every time this highly privileged role is used. This process reduces the risk of an attacker elevating a compromised account to the highest privilege level, as any activation must first be reviewed and approved by a trusted party.", + "ImpactStatement": "Approvers do not need to be assigned the same role or be members of the same group. It's important to have at least two approvers and an emergency access (break-glass) account to prevent a scenario where no Global Administrators are available.", + "RemediationProcedure": "To remediate using the UI: 1. Navigate to Microsoft Entra admin center https://entra.microsoft.com/. 2. Click to expand Identity Governance select Privileged Identity Management. 3. Under Manage select Microsoft Entra Roles. 4. Under Manage select Roles. 5. Select Global Administrator in the list. 6. Select Role settings and click Edit. 7. Check the Require approval to activate box. 8. Add at least two approvers. 9. Click Update.", + "AuditProcedure": "To audit using the UI: 1. Navigate to Microsoft Entra admin center https://entra.microsoft.com/. 2. Click to expand Identity Governance select Privileged Identity Management. 3. Under Manage select Microsoft Entra Roles. 4. Under Manage select Roles. 5. Select Global Administrator in the list. 6. Select Role settings. 7. Verify Require approval to activate is set to Yes. 8. Verify there are at least two approvers in the list.", + "AdditionalInformation": "This only acts as protection for eligible users that are activating a role. Directly assigning a role does require an approval workflow so therefore it is important to implement and use PIM correctly.", + "References": "https://learn.microsoft.com/en-us/entra/id-governance/privileged-identity-management/pim-configure", + "DefaultValue": "Require approval to activate: No" + } + ] + }, + { + "Id": "5.3.5", + "Description": "Microsoft Entra Privileged Identity Management can be used to audit roles, allow just in time activation of roles and allow for periodic role attestation. Requiring approval before activation allows one of the selected approvers to first review and then approve the activation prior to PIM granted the role. Ensure approval is required for Privileged Role Administrator activation.", + "Checks": [], + "Attributes": [ + { + "Section": "5 Microsoft Entra admin center", + "SubSection": "5.3 ID Governance", + "Profile": "E5 Level 1", + "AssessmentStatus": "Automated", + "Description": "Microsoft Entra Privileged Identity Management can be used to audit roles, allow just in time activation of roles and allow for periodic role attestation. Requiring approval before activation allows one of the selected approvers to first review and then approve the activation prior to PIM granted the role. Ensure approval is required for Privileged Role Administrator activation.", + "RationaleStatement": "Requiring approval for Privileged Role Administrator role activation enhances visibility and accountability every time this highly privileged role is used. This process reduces the risk of an attacker elevating a compromised account to the highest privilege level, as any activation must first be reviewed and approved by a trusted party. The Privileged Role Administrator can manage role assignments in Microsoft Entra ID, as well as within Microsoft Entra Privileged Identity Management. They can create and manage groups that can be assigned to Microsoft Entra roles. Additionally, this role allows management of all aspects of Privileged Identity Management and administrative units.", + "ImpactStatement": "Approvers do not need to be assigned the same role or be members of the same group. It's important to have at least two approvers and an emergency access (break-glass) account to prevent a scenario where no Privileged Role Administrators are available.", + "RemediationProcedure": "To remediate using the UI: 1. Navigate to Microsoft Entra admin center https://entra.microsoft.com/. 2. Click to expand Identity Governance select Privileged Identity Management. 3. Under Manage select Microsoft Entra Roles. 4. Under Manage select Roles. 5. Select Privileged Role Administrator in the list. 6. Select Role settings and click Edit. 7. Check the Require approval to activate box. 8. Add at least two approvers. 9. Click Update.", + "AuditProcedure": "To audit using the UI: 1. Navigate to Microsoft Entra admin center https://entra.microsoft.com/. 2. Click to expand Identity Governance select Privileged Identity Management. 3. Under Manage select Microsoft Entra Roles. 4. Under Manage select Roles. 5. Select Privileged Role Administrator in the list. 6. Select Role settings. 7. Verify Require approval to activate is set to Yes. 8. Verify there are at least two approvers in the list.", + "AdditionalInformation": "This only acts as protection for eligible users that are activating a role. Directly assigning a role does require an approval workflow so therefore it is important to implement and use PIM correctly.", + "References": "https://learn.microsoft.com/en-us/entra/id-governance/privileged-identity-management/pim-configure", + "DefaultValue": "Require approval to activate: No" + } + ] + }, + { + "Id": "6.1.1", + "Description": "The value False indicates that mailbox auditing on by default is turned on for the organization. Mailbox auditing on by default in the organization overrides the mailbox auditing settings on individual mailboxes. Ensure 'AuditDisabled' organizationally is set to 'False'.", + "Checks": [ + "exchange_organization_mailbox_auditing_enabled" + ], + "Attributes": [ + { + "Section": "6 Exchange admin center", + "SubSection": "6.1 Audit", + "Profile": "E3 Level 1", + "AssessmentStatus": "Automated", + "Description": "The value False indicates that mailbox auditing on by default is turned on for the organization. Mailbox auditing on by default in the organization overrides the mailbox auditing settings on individual mailboxes. Ensure 'AuditDisabled' organizationally is set to 'False'.", + "RationaleStatement": "Enforcing the default ensures auditing was not turned off intentionally or accidentally. Auditing mailbox actions will allow forensics and IR teams to trace various malicious activities that can generate TTPs caused by inbox access and tampering.", + "ImpactStatement": "None - this is the default behavior as of 2019.", + "RemediationProcedure": "To remediate using PowerShell: 1. Connect to Exchange Online using Connect-ExchangeOnline. 2. Run the following PowerShell command: Set-OrganizationConfig -AuditDisabled $false", + "AuditProcedure": "To audit using PowerShell: 1. Connect to Exchange Online using Connect-ExchangeOnline. 2. Run the following PowerShell command: Get-OrganizationConfig | Format-List AuditDisabled. 3. Ensure AuditDisabled is set to False.", + "AdditionalInformation": "Without advanced auditing (E5 function) the logs are limited to 90 days.", + "References": "https://learn.microsoft.com/en-us/purview/audit-mailboxes?view=o365-worldwide", + "DefaultValue": "False" + } + ] + }, + { + "Id": "6.1.2", + "Description": "Mailbox audit logging is turned on by default in all organizations. This means that certain actions performed by mailbox owners, delegates, and admins are automatically logged. Ensure mailbox audit actions are configured with additional actions beyond defaults.", + "Checks": [ + "exchange_user_mailbox_auditing_enabled" + ], + "Attributes": [ + { + "Section": "6 Exchange admin center", + "SubSection": "6.1 Audit", + "Profile": "E3 Level 1", + "AssessmentStatus": "Automated", + "Description": "Mailbox audit logging is turned on by default in all organizations. This means that certain actions performed by mailbox owners, delegates, and admins are automatically logged. Ensure mailbox audit actions are configured with additional actions beyond defaults.", + "RationaleStatement": "Whether it is for regulatory compliance or for tracking unauthorized configuration changes in Microsoft 365, enabling mailbox auditing and ensuring the proper mailbox actions are accounted for allows for Microsoft 365 teams to run security operations, forensics or general investigations on mailbox activities.", + "ImpactStatement": "Adding additional audit action types and increasing the AuditLogAgeLimit from 90 to 180 days will have a limited impact on mailbox storage. Mailbox audit log records are stored in a subfolder (named Audits) in the Recoverable Items folder in each user's mailbox.", + "RemediationProcedure": "To remediate using PowerShell: 1. Connect to Exchange Online using Connect-ExchangeOnline. 2. For each UserMailbox ensure AuditEnabled is True and include additional audit actions: Admin actions: Copy, FolderBind and Move. Delegate actions: FolderBind and Move. Owner actions: Create, MailboxLogin and Move. Set AuditLogAgeLimit to 180 days.", + "AuditProcedure": "To audit using PowerShell: 1. Connect to Exchange Online using Connect-ExchangeOnline. 2. Inspect each UserMailbox and ensure AuditEnabled is True and the following audit actions are included in addition to default actions of each sign-in type: Admin actions: Copy, FolderBind and Move. Delegate actions: FolderBind and Move. Owner actions: Create, MailboxLogin and Move.", + "AdditionalInformation": "Audit (Standard) licensing allows for up to 180 days log retention as of October 2023. Mailboxes with Audit (Premium) licenses, which is included with E5, can retain audit logs beyond 180 days.", + "References": "https://learn.microsoft.com/en-us/purview/audit-mailboxes?view=o365-worldwide", + "DefaultValue": "AuditEnabled: True for all mailboxes except Resource Mailboxes, Public Folder Mailboxes, and DiscoverySearch Mailbox" + } + ] + }, + { + "Id": "6.1.3", + "Description": "When configuring a user or computer account to bypass mailbox audit logging, the system will not record any access, or actions performed by the said user or computer account on any mailbox. Ensure 'AuditBypassEnabled' is not enabled on mailboxes.", + "Checks": [ + "exchange_mailbox_audit_bypass_disabled" + ], + "Attributes": [ + { + "Section": "6 Exchange admin center", + "SubSection": "6.1 Audit", + "Profile": "E3 Level 1", + "AssessmentStatus": "Automated", + "Description": "When configuring a user or computer account to bypass mailbox audit logging, the system will not record any access, or actions performed by the said user or computer account on any mailbox. Ensure 'AuditBypassEnabled' is not enabled on mailboxes.", + "RationaleStatement": "If a mailbox audit bypass association is added for an account, the account can access any mailbox in the organization to which it has been assigned access permissions, without generating any mailbox audit logging entries for such access or recording any actions taken, such as message deletions. Enabling this parameter, whether intentionally or unintentionally, could allow insiders or malicious actors to conceal their activity on specific mailboxes.", + "ImpactStatement": "None - this is the default behavior.", + "RemediationProcedure": "To remediate using PowerShell: 1. Connect to Exchange Online using Connect-ExchangeOnline. 2. Run the following to disable AuditBypass for mailboxes which currently have it enabled: Get-MailboxAuditBypassAssociation -ResultSize unlimited | Where-Object { $_.AuditBypassEnabled -eq $true } | ForEach-Object { Set-MailboxAuditBypassAssociation -Identity $_.Name -AuditBypassEnabled $false }", + "AuditProcedure": "To audit using PowerShell: 1. Connect to Exchange Online using Connect-ExchangeOnline. 2. Run the following PowerShell command: Get-MailboxAuditBypassAssociation -ResultSize unlimited | Where-Object {$_.AuditBypassEnabled -eq $true} | Select-Object Name,AuditBypassEnabled. 3. If nothing is returned, then there are no accounts with Audit Bypass enabled.", + "AdditionalInformation": "", + "References": "https://learn.microsoft.com/en-us/powershell/module/exchange/get-mailboxauditbypassassociation", + "DefaultValue": "AuditBypassEnabled: False" + } + ] + }, + { + "Id": "6.2.1", + "Description": "Exchange Online offers several methods of managing the flow of email messages including Remote domain, Transport Rules, and Anti-spam outbound policies. Ensure all forms of mail forwarding are blocked and/or disabled.", + "Checks": [ + "exchange_transport_rules_mail_forwarding_disabled" + ], + "Attributes": [ + { + "Section": "6 Exchange admin center", + "SubSection": "6.2 Mail flow", + "Profile": "E3 Level 1", + "AssessmentStatus": "Automated", + "Description": "Exchange Online offers several methods of managing the flow of email messages including Remote domain, Transport Rules, and Anti-spam outbound policies. Ensure all forms of mail forwarding are blocked and/or disabled.", + "RationaleStatement": "Attackers often create these rules to exfiltrate data from your tenancy, this could be accomplished via access to an end-user account or otherwise. An insider could also use one of these methods as a secondary channel to exfiltrate sensitive data.", + "ImpactStatement": "Care should be taken before implementation to ensure there is no business need for case-by-case auto-forwarding. Disabling auto-forwarding to remote domains will affect all users in an organization. Any exclusions should be implemented based on organizational policy.", + "RemediationProcedure": "STEP 1: Transport rules - Remove any transport rules that redirect email to external domains. STEP 2: Anti-spam outbound policy - Navigate to Microsoft 365 Defender https://security.microsoft.com/. Expand E-mail & collaboration then select Policies & rules. Select Threat policies > Anti-spam. For Anti-spam outbound policy (default) and any custom policies, set Automatic forwarding to Off - Forwarding is disabled.", + "AuditProcedure": "STEP 1: Transport rules - Review transport rules and verify that none of them forward or redirect e-mail to external domains. Run: Get-TransportRule | Where-Object {$_.RedirectMessageTo -ne $null} | ft Name,RedirectMessageTo. STEP 2: Anti-spam outbound policy - Navigate to Microsoft 365 Defender. Inspect Anti-spam outbound policy (default) and ensure Automatic forwarding is set to Off - Forwarding is disabled.", + "AdditionalInformation": "Any exclusions should be implemented based on organizational policy.", + "References": "https://learn.microsoft.com/en-us/defender-office-365/outbound-spam-policies-external-email-forwarding", + "DefaultValue": "AutoForwardingMode varies by policy" + } + ] + }, + { + "Id": "6.2.2", + "Description": "Mail flow rules (transport rules) in Exchange Online are used to identify and take action on messages that flow through the organization. Ensure mail transport rules do not whitelist specific domains.", + "Checks": [ + "exchange_transport_rules_whitelist_disabled" + ], + "Attributes": [ + { + "Section": "6 Exchange admin center", + "SubSection": "6.2 Mail flow", + "Profile": "E3 Level 1", + "AssessmentStatus": "Automated", + "Description": "Mail flow rules (transport rules) in Exchange Online are used to identify and take action on messages that flow through the organization. Ensure mail transport rules do not whitelist specific domains.", + "RationaleStatement": "Whitelisting domains in transport rules bypasses regular malware and phishing scanning, which can enable an attacker to launch attacks against your users from a safe haven domain.", + "ImpactStatement": "Care should be taken before implementation to ensure there is no business need for case-by-case whitelisting. Removing all whitelisted domains could affect incoming mail flow to an organization although modern systems sending legitimate mail should have no issue with this.", + "RemediationProcedure": "To remediate using the UI: 1. Navigate to Exchange admin center https://admin.exchange.microsoft.com. 2. Click to expand Mail Flow and then select Rules. 3. For each rule that sets the spam confidence level to -1 for a specific domain, select the rule and click Delete.", + "AuditProcedure": "To audit using the UI: 1. Navigate to Exchange admin center https://admin.exchange.microsoft.com. 2. Click to expand Mail Flow and then select Rules. 3. Review each rule and ensure that a single rule does not contain both: Under Apply this rule if: Sender's address domain portion belongs to any of these domains AND Under Do the following: Set the spam confidence level (SCL) to '-1'.", + "AdditionalInformation": "Setting the spam confidence level to -1 indicates the message is from a trusted sender, so the message bypasses spam filtering. If an organization identifies a business need for an exception, the domain should only be whitelisted if inbound emails from that domain originate from a specific IP address.", + "References": "https://learn.microsoft.com/en-us/exchange/security-and-compliance/mail-flow-rules/mail-flow-rules", + "DefaultValue": "No transport rules with whitelisted domains by default" + } + ] + }, + { + "Id": "6.2.3", + "Description": "External callouts provide a native experience to identify emails from senders outside the organization. This is achieved by presenting a new tag on emails called 'External'. Ensure email from external senders is identified.", + "Checks": [ + "exchange_external_email_tagging_enabled" + ], + "Attributes": [ + { + "Section": "6 Exchange admin center", + "SubSection": "6.2 Mail flow", + "Profile": "E3 Level 1", + "AssessmentStatus": "Automated", + "Description": "External callouts provide a native experience to identify emails from senders outside the organization. This is achieved by presenting a new tag on emails called 'External'. Ensure email from external senders is identified.", + "RationaleStatement": "Tagging emails from external senders helps to inform end users about the origin of the email. This can allow them to proceed with more caution and make informed decisions when it comes to identifying spam or phishing emails.", + "ImpactStatement": "Mail flow rules using external tagging must be disabled, along with third-party mail filtering tools that offer similar features, to avoid duplicate [External] tags. External tags can consume additional screen space on systems with limited real estate. After enabling this feature via PowerShell, it may take 24-48 hours for users to see the External sender tag.", + "RemediationProcedure": "To remediate using PowerShell: 1. Connect to Exchange online using Connect-ExchangeOnline. 2. Run the following PowerShell command: Set-ExternalInOutlook -Enabled $true", + "AuditProcedure": "To audit using PowerShell: 1. Connect to Exchange online using Connect-ExchangeOnline. 2. Run the following PowerShell command: Get-ExternalInOutlook. 3. For each identity verify Enabled is set to True and the AllowList only contains email addresses the organization has permitted to bypass external tagging.", + "AdditionalInformation": "Existing emails in a user's inbox from external senders are not tagged retroactively. Third-party tools that provide similar functionality will also meet compliance requirements.", + "References": "https://learn.microsoft.com/en-us/powershell/module/exchange/set-externalinoutlook", + "DefaultValue": "Disabled (False)" + } + ] + }, + { + "Id": "6.3.1", + "Description": "Specify the administrators and users who can install and manage add-ins for Outlook in Exchange Online. By default, users can install add-ins in their Microsoft Outlook Desktop client. Ensure users installing Outlook add-ins is not allowed.", + "Checks": [ + "exchange_roles_assignment_policy_addins_disabled" + ], + "Attributes": [ + { + "Section": "6 Exchange admin center", + "SubSection": "6.3 Roles", + "Profile": "E3 Level 2", + "AssessmentStatus": "Automated", + "Description": "Specify the administrators and users who can install and manage add-ins for Outlook in Exchange Online. By default, users can install add-ins in their Microsoft Outlook Desktop client. Ensure users installing Outlook add-ins is not allowed.", + "RationaleStatement": "Attackers exploit vulnerable or custom add-ins to access user data. Disabling user-installed add-ins in Microsoft Outlook reduces this threat surface.", + "ImpactStatement": "Implementing this change will impact both end users and administrators. End users will be unable to integrate third-party applications they desire, and administrators may receive requests to grant permission for necessary third-party apps.", + "RemediationProcedure": "To remediate using the UI: 1. Navigate to Exchange admin center https://admin.exchange.microsoft.com. 2. Click to expand Roles select User roles. 3. Select Default Role Assignment Policy. 4. In the properties pane on the right click on Manage permissions. 5. Under Other roles uncheck My Custom Apps, My Marketplace Apps and My ReadWriteMailbox Apps. 6. Click Save changes.", + "AuditProcedure": "To audit using the UI: 1. Navigate to Exchange admin center https://admin.exchange.microsoft.com. 2. Click to expand Roles select User roles. 3. Select Default Role Assignment Policy. 4. In the properties pane on the right click on Manage permissions. 5. Under Other roles verify My Custom Apps, My Marketplace Apps and My ReadWriteMailbox Apps are unchecked.", + "AdditionalInformation": "", + "References": "https://learn.microsoft.com/en-us/exchange/clients-and-mobile-in-exchange-online/add-ins-for-outlook/specify-who-can-install-and-manage-addins", + "DefaultValue": "My Custom Apps, My Marketplace Apps, and My ReadWriteMailbox Apps are checked" + } + ] + }, + { + "Id": "6.5.1", + "Description": "Modern authentication in Microsoft 365 enables authentication features like multifactor authentication (MFA) using smart cards, certificate-based authentication (CBA), and third-party SAML identity providers. Ensure modern authentication for Exchange Online is enabled.", + "Checks": [ + "exchange_organization_modern_authentication_enabled" + ], + "Attributes": [ + { + "Section": "6 Exchange admin center", + "SubSection": "6.5 Settings", + "Profile": "E3 Level 1", + "AssessmentStatus": "Automated", + "Description": "Modern authentication in Microsoft 365 enables authentication features like multifactor authentication (MFA) using smart cards, certificate-based authentication (CBA), and third-party SAML identity providers. Ensure modern authentication for Exchange Online is enabled.", + "RationaleStatement": "Strong authentication controls, such as the use of multifactor authentication, may be circumvented if basic authentication is used by Exchange Online email clients such as Outlook 2016 and Outlook 2013. Enabling modern authentication for Exchange Online ensures strong authentication mechanisms are used when establishing sessions between email clients and Exchange Online.", + "ImpactStatement": "Users of older email clients, such as Outlook 2013 and Outlook 2016, will no longer be able to authenticate to Exchange using Basic Authentication, which will necessitate migration to modern authentication practices.", + "RemediationProcedure": "To remediate using the UI: 1. Navigate to Microsoft 365 admin center https://admin.microsoft.com. 2. Click to expand Settings select Org Settings. 3. Select Modern authentication. 4. Check Turn on modern authentication for Outlook 2013 for Windows and later (recommended) to enable modern authentication.", + "AuditProcedure": "To audit using the UI: 1. Navigate to Microsoft 365 admin center https://admin.microsoft.com. 2. Click to expand Settings select Org Settings. 3. Select Modern authentication. 4. Verify Turn on modern authentication for Outlook 2013 for Windows and later (recommended) is checked.", + "AdditionalInformation": "", + "References": "https://learn.microsoft.com/en-us/exchange/clients-and-mobile-in-exchange-online/enable-or-disable-modern-authentication-in-exchange-online", + "DefaultValue": "True" + } + ] + }, + { + "Id": "6.5.2", + "Description": "MailTips are informative messages displayed to users while they're composing a message. While a new message is open and being composed, Exchange analyzes the message (including recipients). If a potential problem is detected, the user is notified with a MailTip prior to sending the message. Ensure MailTips are enabled for end users.", + "Checks": [ + "exchange_organization_mailtips_enabled" + ], + "Attributes": [ + { + "Section": "6 Exchange admin center", + "SubSection": "6.5 Settings", + "Profile": "E3 Level 1", + "AssessmentStatus": "Automated", + "Description": "MailTips are informative messages displayed to users while they're composing a message. While a new message is open and being composed, Exchange analyzes the message (including recipients). If a potential problem is detected, the user is notified with a MailTip prior to sending the message. Ensure MailTips are enabled for end users.", + "RationaleStatement": "Setting up MailTips gives a visual aid to users when they send emails to large groups of recipients or send emails to recipients not within the tenant.", + "ImpactStatement": "Not applicable.", + "RemediationProcedure": "To remediate using PowerShell: 1. Connect to Exchange Online using Connect-ExchangeOnline. 2. Run the following PowerShell command: Set-OrganizationConfig -MailTipsAllTipsEnabled $true -MailTipsExternalRecipientsTipsEnabled $true -MailTipsGroupMetricsEnabled $true -MailTipsLargeAudienceThreshold 25", + "AuditProcedure": "To audit using PowerShell: 1. Connect to Exchange Online using Connect-ExchangeOnline. 2. Run the following PowerShell command: Get-OrganizationConfig | fl MailTips*. 3. Verify the values for MailTipsAllTipsEnabled, MailTipsExternalRecipientsTipsEnabled, and MailTipsGroupMetricsEnabled are set to True and MailTipsLargeAudienceThreshold is set to an acceptable value; 25 is the default value.", + "AdditionalInformation": "", + "References": "https://learn.microsoft.com/en-us/exchange/clients-and-mobile-in-exchange-online/mailtips/mailtips", + "DefaultValue": "MailTipsAllTipsEnabled: True, MailTipsExternalRecipientsTipsEnabled: False, MailTipsGroupMetricsEnabled: True, MailTipsLargeAudienceThreshold: 25" + } + ] + }, + { + "Id": "6.5.3", + "Description": "This setting allows users to open certain external files while working in Outlook on the web. If allowed, keep in mind that Microsoft doesn't control the use terms or privacy policies of those third-party services. Ensure additional storage providers are restricted in Outlook on the web.", + "Checks": [ + "exchange_mailbox_policy_additional_storage_restricted" + ], + "Attributes": [ + { + "Section": "6 Exchange admin center", + "SubSection": "6.5 Settings", + "Profile": "E3 Level 2", + "AssessmentStatus": "Automated", + "Description": "This setting allows users to open certain external files while working in Outlook on the web. If allowed, keep in mind that Microsoft doesn't control the use terms or privacy policies of those third-party services. Ensure additional storage providers are restricted in Outlook on the web.", + "RationaleStatement": "By default, additional storage providers are allowed in Office on the Web (such as Box, Dropbox, Facebook, Google Drive, OneDrive Personal, etc.). This could lead to information leakage and additional risk of infection from organizational non-trusted storage providers. Restricting this will inherently reduce risk as it will narrow opportunities for infection and data leakage.", + "ImpactStatement": "The impact associated with this change is highly dependent upon current practices in the tenant. If users do not use other storage providers, then minimal impact is likely. However, if users do regularly utilize providers outside of the tenant this will affect their ability to continue to do so.", + "RemediationProcedure": "To remediate using PowerShell: 1. Connect to Exchange Online using Connect-ExchangeOnline. 2. Run the following PowerShell command: Set-OwaMailboxPolicy -Identity OwaMailboxPolicy-Default -AdditionalStorageProvidersAvailable $false", + "AuditProcedure": "To audit using PowerShell: 1. Connect to Exchange Online using Connect-ExchangeOnline. 2. Run the following PowerShell command to audit the default OWA policy: Get-OwaMailboxPolicy -Identity OwaMailboxPolicy-Default | fl AdditionalStorageProvidersAvailable. 3. Verify that AdditionalStorageProvidersAvailable is False.", + "AdditionalInformation": "", + "References": "https://learn.microsoft.com/en-us/powershell/module/exchange/set-owamailboxpolicy", + "DefaultValue": "AdditionalStorageProvidersAvailable: True" + } + ] + }, + { + "Id": "6.5.4", + "Description": "This setting enables or disables authenticated client SMTP submission (SMTP AUTH) at an organization level in Exchange Online. Ensure SMTP AUTH is disabled.", + "Checks": [ + "exchange_transport_config_smtp_auth_disabled" + ], + "Attributes": [ + { + "Section": "6 Exchange admin center", + "SubSection": "6.5 Settings", + "Profile": "E3 Level 1", + "AssessmentStatus": "Automated", + "Description": "This setting enables or disables authenticated client SMTP submission (SMTP AUTH) at an organization level in Exchange Online. Ensure SMTP AUTH is disabled.", + "RationaleStatement": "SMTP AUTH is a legacy protocol. Disabling it at the organization level supports the principle of least functionality and serves to further back additional controls that block legacy protocols, such as in Conditional Access. Virtually all modern email clients that connect to Exchange Online mailboxes in Microsoft 365 can do so without using SMTP AUTH.", + "ImpactStatement": "This enforces the default behavior, so no impact is expected unless the organization is using it globally. A per-mailbox setting exists that overrides the tenant-wide setting, allowing an individual mailbox SMTP AUTH capability for special cases.", + "RemediationProcedure": "To remediate using the UI: 1. Navigate to Exchange admin center https://admin.exchange.microsoft.com. 2. Select Settings > Mail flow. 3. Check Turn off SMTP AUTH protocol for your organization to disable the protocol.", + "AuditProcedure": "To audit using the UI: 1. Navigate to Exchange admin center https://admin.exchange.microsoft.com. 2. Select Settings > Mail flow. 3. Ensure Turn off SMTP AUTH protocol for your organization is checked.", + "AdditionalInformation": "", + "References": "https://learn.microsoft.com/en-us/exchange/clients-and-mobile-in-exchange-online/authenticated-client-smtp-submission", + "DefaultValue": "SmtpClientAuthenticationDisabled: True" + } + ] + }, + { + "Id": "6.5.5", + "Description": "Direct Send is a method used to send emails directly to an Exchange Online customer's hosted mailboxes from on-premises devices, applications, or third-party cloud services. This method does not require any form of authentication. Ensure Direct Send submissions are rejected.", + "Checks": [], + "Attributes": [ + { + "Section": "6 Exchange admin center", + "SubSection": "6.5 Settings", + "Profile": "E3 Level 2", + "AssessmentStatus": "Automated", + "Description": "Direct Send is a method used to send emails directly to an Exchange Online customer's hosted mailboxes from on-premises devices, applications, or third-party cloud services. This method does not require any form of authentication. Ensure Direct Send submissions are rejected.", + "RationaleStatement": "Direct Send allows devices and applications to transmit unauthenticated email directly to Exchange Online. While this method may support legacy systems such as printers or scanners, it introduces significant security risks: Unauthenticated Email Delivery, Phishing and Spoofing Risks, and Lack of Visibility and Control. Threat research has shown that attackers are actively exploiting Direct Send to impersonate internal accounts and distribute malicious content without needing to compromise any credentials.", + "ImpactStatement": "Microsoft has identified some known issues with disabling Direct Send including forwarding scenarios and Azure Communication Services (ACS) traffic. Care should be taken before implementation.", + "RemediationProcedure": "To remediate using PowerShell: 1. Connect to Exchange Online using Connect-ExchangeOnline. 2. Run the following PowerShell command: Set-OrganizationConfig -RejectDirectSend $true", + "AuditProcedure": "To audit using PowerShell: 1. Connect to Exchange Online using Connect-ExchangeOnline. 2. Run the following PowerShell command: Get-OrganizationConfig | fl RejectDirectSend. 3. Verify that the value returned for RejectDirectSend is True.", + "AdditionalInformation": "", + "References": "https://techcommunity.microsoft.com/blog/exchange/introducing-more-control-over-direct-send-in-exchange-online/4408790", + "DefaultValue": "RejectDirectSend: False" + } + ] + }, + { + "Id": "7.2.1", + "Description": "Modern authentication in Microsoft 365 enables authentication features like multifactor authentication (MFA) using smart cards, certificate-based authentication (CBA), and third-party SAML identity providers. Ensure modern authentication for SharePoint applications is required.", + "Checks": [ + "sharepoint_modern_authentication_required" + ], + "Attributes": [ + { + "Section": "7 SharePoint admin center", + "SubSection": "7.2 Policies", + "Profile": "E3 Level 1", + "AssessmentStatus": "Automated", + "Description": "Modern authentication in Microsoft 365 enables authentication features like multifactor authentication (MFA) using smart cards, certificate-based authentication (CBA), and third-party SAML identity providers. Ensure modern authentication for SharePoint applications is required.", + "RationaleStatement": "Strong authentication controls, such as the use of multifactor authentication, may be circumvented if basic authentication is used by SharePoint applications. Requiring modern authentication for SharePoint applications ensures strong authentication mechanisms are used when establishing sessions between these applications, SharePoint, and connecting users.", + "ImpactStatement": "Implementation of modern authentication for SharePoint will require users to authenticate to SharePoint using modern authentication. This may cause a minor impact to typical user behavior. This may also prevent third-party apps from accessing SharePoint Online resources and will block apps using the SharePointOnlineCredentials class.", + "RemediationProcedure": "To remediate using the UI: 1. Navigate to SharePoint admin center https://admin.microsoft.com/sharepoint. 2. Click to expand Policies select Access control. 3. Select Apps that don't use modern authentication. 4. Select the radio button for Block access. 5. Click Save.", + "AuditProcedure": "To audit using the UI: 1. Navigate to SharePoint admin center https://admin.microsoft.com/sharepoint. 2. Click to expand Policies select Access control. 3. Select Apps that don't use modern authentication and ensure that it is set to Block access.", + "AdditionalInformation": "", + "References": "https://learn.microsoft.com/en-us/powershell/module/sharepoint-online/set-spotenant", + "DefaultValue": "True (Apps that don't use modern authentication are allowed)" + } + ] + }, + { + "Id": "7.2.2", + "Description": "Entra ID B2B provides authentication and management of guests. Authentication happens via one-time passcode when they don't already have a work or school account or a Microsoft account. Ensure SharePoint and OneDrive integration with Azure AD B2B is enabled.", + "Checks": [ + "sharepoint_guest_sharing_restricted" + ], + "Attributes": [ + { + "Section": "7 SharePoint admin center", + "SubSection": "7.2 Policies", + "Profile": "E3 Level 1", + "AssessmentStatus": "Automated", + "Description": "Entra ID B2B provides authentication and management of guests. Authentication happens via one-time passcode when they don't already have a work or school account or a Microsoft account. Ensure SharePoint and OneDrive integration with Azure AD B2B is enabled.", + "RationaleStatement": "External users assigned guest accounts will be subject to Entra ID access policies, such as multi-factor authentication. This provides a way to manage guest identities and control access to SharePoint and OneDrive resources. Without this integration, files can be shared without account registration, making it more challenging to audit and manage who has access to the organization's data.", + "ImpactStatement": "B2B collaboration is used with other Entra services so should not be new or unusual. Microsoft also has made the experience seamless when turning on integration on SharePoint sites that already have active files shared with guest users.", + "RemediationProcedure": "To remediate using PowerShell: 1. Connect to SharePoint Online using Connect-SPOService. 2. Run the following command: Set-SPOTenant -EnableAzureADB2BIntegration $true", + "AuditProcedure": "To audit using PowerShell: 1. Connect to SharePoint Online using Connect-SPOService. 2. Run the following command: Get-SPOTenant | ft EnableAzureADB2BIntegration. 3. Ensure the returned value is True.", + "AdditionalInformation": "Global Reader role currently can't access SharePoint using PowerShell.", + "References": "https://learn.microsoft.com/en-us/sharepoint/sharepoint-azureb2b-integration", + "DefaultValue": "False" + } + ] + }, + { + "Id": "7.2.3", + "Description": "The external sharing settings govern sharing for the organization overall. Each site has its own sharing setting that can be set independently, though it must be at the same or more restrictive setting as the organization. Ensure external content sharing is restricted.", + "Checks": [ + "sharepoint_external_sharing_managed" + ], + "Attributes": [ + { + "Section": "7 SharePoint admin center", + "SubSection": "7.2 Policies", + "Profile": "E3 Level 1", + "AssessmentStatus": "Automated", + "Description": "The external sharing settings govern sharing for the organization overall. Each site has its own sharing setting that can be set independently, though it must be at the same or more restrictive setting as the organization. Ensure external content sharing is restricted to New and existing guests or less permissive.", + "RationaleStatement": "Forcing guest authentication on the organization's tenant enables the implementation of controls and oversight over external file sharing. When a guest is registered with the organization, they now have an identity which can be accounted for. This identity can also have other restrictions applied to it through group membership and conditional access rules.", + "ImpactStatement": "When using B2B integration, Entra ID external collaboration settings, such as guest invite settings and collaboration restrictions apply.", + "RemediationProcedure": "To remediate using the UI: 1. Navigate to SharePoint admin center https://admin.microsoft.com/sharepoint. 2. Click to expand Policies > Sharing. 3. Locate the External sharing section. 4. Under SharePoint, move the slider bar to New and existing guests or a less permissive level. OneDrive will also be moved to the same level and can never be more permissive than SharePoint.", + "AuditProcedure": "To audit using the UI: 1. Navigate to SharePoint admin center https://admin.microsoft.com/sharepoint. 2. Click to expand Policies > Sharing. 3. Locate the External sharing section. 4. Under SharePoint, ensure the slider bar is set to New and existing guests or a less permissive level.", + "AdditionalInformation": "", + "References": "https://learn.microsoft.com/en-us/sharepoint/turn-external-sharing-on-or-off", + "DefaultValue": "Anyone (ExternalUserAndGuestSharing)" + } + ] + }, + { + "Id": "7.2.4", + "Description": "This setting governs the global permissiveness of OneDrive content sharing in the organization. OneDrive content sharing can be restricted independent of SharePoint but can never be more permissive than the level established with SharePoint. Ensure OneDrive content sharing is restricted.", + "Checks": [], + "Attributes": [ + { + "Section": "7 SharePoint admin center", + "SubSection": "7.2 Policies", + "Profile": "E3 Level 2", + "AssessmentStatus": "Automated", + "Description": "This setting governs the global permissiveness of OneDrive content sharing in the organization. OneDrive content sharing can be restricted independent of SharePoint but can never be more permissive than the level established with SharePoint. Ensure OneDrive content sharing is restricted to Only people in your organization.", + "RationaleStatement": "OneDrive, designed for end-user cloud storage, inherently provides less oversight and control compared to SharePoint, which often involves additional content overseers or site administrators. This autonomy can lead to potential risks such as inadvertent sharing of privileged information by end users. Restricting external OneDrive sharing will require users to transfer content to SharePoint folders first which have those tighter controls.", + "ImpactStatement": "Users will be required to take additional steps to share OneDrive content or use other official channels.", + "RemediationProcedure": "To remediate using the UI: 1. Navigate to SharePoint admin center https://admin.microsoft.com/sharepoint. 2. Click to expand Policies > Sharing. 3. Locate the External sharing section. 4. Under OneDrive, set the slider bar to Only people in your organization.", + "AuditProcedure": "To audit using the UI: 1. Navigate to SharePoint admin center https://admin.microsoft.com/sharepoint. 2. Click to expand Policies > Sharing. 3. Locate the External sharing section. 4. Under OneDrive, ensure the slider bar is set to Only people in your organization.", + "AdditionalInformation": "", + "References": "https://learn.microsoft.com/en-us/powershell/module/sharepoint-online/set-spotenant#-onedrivesharingcapability", + "DefaultValue": "Anyone (ExternalUserAndGuestSharing)" + } + ] + }, + { + "Id": "7.2.5", + "Description": "SharePoint gives users the ability to share files, folders, and site collections. Internal users can share with external collaborators, and with the right permissions could share to other external parties. Ensure that SharePoint guest users cannot share items they don't own.", + "Checks": [], + "Attributes": [ + { + "Section": "7 SharePoint admin center", + "SubSection": "7.2 Policies", + "Profile": "E3 Level 2", + "AssessmentStatus": "Automated", + "Description": "SharePoint gives users the ability to share files, folders, and site collections. Internal users can share with external collaborators, and with the right permissions could share to other external parties. Ensure that SharePoint guest users cannot share items they don't own.", + "RationaleStatement": "Sharing and collaboration are key; however, file, folder, or site collection owners should have the authority over what external users get shared with to prevent unauthorized disclosures of information.", + "ImpactStatement": "The impact associated with this change is highly dependent upon current practices. If users do not regularly share with external parties, then minimal impact is likely. However, if users do regularly share with guests/externally, minimum impacts could occur as those external users will be unable to 're-share' content.", + "RemediationProcedure": "To remediate using the UI: 1. Navigate to SharePoint admin center https://admin.microsoft.com/sharepoint. 2. Click to expand Policies then select Sharing. 3. Expand More external sharing settings, uncheck Allow guests to share items they don't own. 4. Click Save.", + "AuditProcedure": "To audit using the UI: 1. Navigate to SharePoint admin center https://admin.microsoft.com/sharepoint. 2. Click to expand Policies then select Sharing. 3. Expand More external sharing settings, verify that Allow guests to share items they don't own is unchecked.", + "AdditionalInformation": "", + "References": "https://learn.microsoft.com/en-us/sharepoint/turn-external-sharing-on-or-off", + "DefaultValue": "Checked (False)" + } + ] + }, + { + "Id": "7.2.6", + "Description": "The external sharing features of SharePoint and OneDrive let users in the organization share content with people outside the organization. Ensure SharePoint external sharing is restricted by limiting external sharing by domain to allow only specific domains.", + "Checks": [ + "sharepoint_external_sharing_restricted" + ], + "Attributes": [ + { + "Section": "7 SharePoint admin center", + "SubSection": "7.2 Policies", + "Profile": "E3 Level 2", + "AssessmentStatus": "Automated", + "Description": "The external sharing features of SharePoint and OneDrive let users in the organization share content with people outside the organization. Ensure SharePoint external sharing is restricted by limiting external sharing by domain to allow only specific domains.", + "RationaleStatement": "Attackers will often attempt to expose sensitive information to external entities through sharing, and restricting the domains that users can share documents with will reduce that surface area.", + "ImpactStatement": "Enabling this feature will prevent users from sharing documents with domains outside of the organization unless allowed.", + "RemediationProcedure": "To remediate using the UI: 1. Navigate to SharePoint admin center https://admin.microsoft.com/sharepoint. 2. Expand Policies then click Sharing. 3. Expand More external sharing settings and check Limit external sharing by domain. 4. Select Add domains to add a list of approved domains. 5. Click Save at the bottom of the page.", + "AuditProcedure": "To audit using the UI: 1. Navigate to SharePoint admin center https://admin.microsoft.com/sharepoint. 2. Expand Policies then click Sharing. 3. Expand More external sharing settings and confirm that Limit external sharing by domain is checked. 4. Click on Add domains and verify the sub setting Allow only specific domains is selected and with an approved list domains.", + "AdditionalInformation": "", + "References": "https://learn.microsoft.com/en-us/sharepoint/turn-external-sharing-on-or-off#more-external-sharing-settings", + "DefaultValue": "Limit external sharing by domain is unchecked, SharingDomainRestrictionMode: None" + } + ] + }, + { + "Id": "7.2.7", + "Description": "This setting sets the default link type that a user will see when sharing content in OneDrive or SharePoint. It does not restrict or exclude any other options. Ensure link sharing is restricted in SharePoint and OneDrive.", + "Checks": [], + "Attributes": [ + { + "Section": "7 SharePoint admin center", + "SubSection": "7.2 Policies", + "Profile": "E3 Level 1", + "AssessmentStatus": "Automated", + "Description": "This setting sets the default link type that a user will see when sharing content in OneDrive or SharePoint. It does not restrict or exclude any other options. Ensure link sharing is restricted to Specific people (only the people the user specifies) or Only people in your organization (more restrictive).", + "RationaleStatement": "By defaulting to specific people, the user will first need to consider whether or not the content being shared should be accessible by the entire organization versus select individuals. This aids in reinforcing the concept of least privilege.", + "ImpactStatement": "Not applicable.", + "RemediationProcedure": "To remediate using the UI: 1. Navigate to SharePoint admin center https://admin.microsoft.com/sharepoint. 2. Click to expand Policies > Sharing. 3. Scroll to File and folder links. 4. Set Choose the type of link that's selected by default when users share files and folders in SharePoint and OneDrive to Specific people (only the people the user specifies) or Only people in your organization.", + "AuditProcedure": "To audit using the UI: 1. Navigate to SharePoint admin center https://admin.microsoft.com/sharepoint. 2. Click to expand Policies > Sharing. 3. Scroll to File and folder links. 4. Ensure that the setting Choose the type of link that's selected by default when users share files and folders in SharePoint and OneDrive is set to Specific people (only the people the user specifies) or Only people in your organization (more restrictive).", + "AdditionalInformation": "", + "References": "https://learn.microsoft.com/en-us/powershell/module/sharepoint-online/set-spotenant", + "DefaultValue": "Only people in your organization (Internal)" + } + ] + }, + { + "Id": "7.2.8", + "Description": "External sharing of content can be restricted to specific security groups. This setting is global, applies to sharing in both SharePoint and OneDrive and cannot be set at the site level in SharePoint. Ensure external sharing is restricted by security group.", + "Checks": [], + "Attributes": [ + { + "Section": "7 SharePoint admin center", + "SubSection": "7.2 Policies", + "Profile": "E3 Level 2", + "AssessmentStatus": "Manual", + "Description": "External sharing of content can be restricted to specific security groups. This setting is global, applies to sharing in both SharePoint and OneDrive and cannot be set at the site level in SharePoint. Ensure external sharing is restricted by security group.", + "RationaleStatement": "Organizations wishing to create tighter security controls for external sharing can set this to enforce role-based access control by using security groups already defined in Microsoft Entra ID.", + "ImpactStatement": "OneDrive will also be governed by this and there is no granular control at the SharePoint site level.", + "RemediationProcedure": "To remediate using the UI: 1. Navigate to SharePoint admin center https://admin.microsoft.com/sharepoint. 2. Click to expand Policies > Sharing. 3. Scroll to and expand More external sharing settings. 4. Check Allow only users in specific security groups to share externally. 5. Define Manage security groups in accordance with company procedure.", + "AuditProcedure": "To audit using the UI: 1. Navigate to SharePoint admin center https://admin.microsoft.com/sharepoint. 2. Click to expand Policies > Sharing. 3. Scroll to and expand More external sharing settings. 4. Verify Allow only users in specific security groups to share externally is checked. 5. Verify Manage security groups is defined and in accordance with company procedure.", + "AdditionalInformation": "Users in these security groups must be allowed to invite guests in the guest invite settings in Microsoft Entra. Identity > External Identities > External collaboration settings.", + "References": "https://learn.microsoft.com/en-us/sharepoint/manage-security-groups", + "DefaultValue": "Unchecked/Undefined" + } + ] + }, + { + "Id": "7.2.9", + "Description": "This policy setting configures the expiration time for each guest that is invited to the SharePoint site or with whom users share individual files and folders with. Ensure guest access to a site or OneDrive will expire automatically after 30 days or less.", + "Checks": [], + "Attributes": [ + { + "Section": "7 SharePoint admin center", + "SubSection": "7.2 Policies", + "Profile": "E3 Level 1", + "AssessmentStatus": "Automated", + "Description": "This policy setting configures the expiration time for each guest that is invited to the SharePoint site or with whom users share individual files and folders with. Ensure guest access to a site or OneDrive will expire automatically after 30 days or less.", + "RationaleStatement": "This setting ensures that guests who no longer need access to the site or link no longer have access after a set period of time. Allowing guest access for an indefinite amount of time could lead to loss of data confidentiality and oversight.", + "ImpactStatement": "Site collection administrators will have to renew access to guests who still need access after 30 days. They will receive an e-mail notification once per week about guest access that is about to expire. The guest expiration policy only applies to guests who use sharing links or guests who have direct permissions to a SharePoint site after the guest policy is enabled.", + "RemediationProcedure": "To remediate using the UI: 1. Navigate to SharePoint admin center https://admin.microsoft.com/sharepoint. 2. Click to expand Policies > Sharing. 3. Scroll to and expand More external sharing settings. 4. Set Guest access to a site or OneDrive will expire automatically after this many days to 30.", + "AuditProcedure": "To audit using the UI: 1. Navigate to SharePoint admin center https://admin.microsoft.com/sharepoint. 2. Click to expand Policies > Sharing. 3. Scroll to and expand More external sharing settings. 4. Ensure Guest access to a site or OneDrive will expire automatically after this many days is checked and set to 30 or less.", + "AdditionalInformation": "Guest membership applies at the Microsoft 365 group level. Guests who have permission to view a SharePoint site or use a sharing link may also have access to a Microsoft Teams team or security group.", + "References": "https://learn.microsoft.com/en-us/sharepoint/turn-external-sharing-on-or-off#change-the-organization-level-external-sharing-setting", + "DefaultValue": "ExternalUserExpirationRequired: False, ExternalUserExpireInDays: 60 days" + } + ] + }, + { + "Id": "7.2.10", + "Description": "This setting configures if guests who use a verification code to access the site or links are required to reauthenticate after a set number of days. Ensure reauthentication with verification code is restricted to 15 days or less.", + "Checks": [], + "Attributes": [ + { + "Section": "7 SharePoint admin center", + "SubSection": "7.2 Policies", + "Profile": "E3 Level 1", + "AssessmentStatus": "Automated", + "Description": "This setting configures if guests who use a verification code to access the site or links are required to reauthenticate after a set number of days. Ensure reauthentication with verification code is restricted to 15 days or less.", + "RationaleStatement": "By increasing the frequency of times guests need to reauthenticate this ensures guest user access to data is not prolonged beyond an acceptable amount of time.", + "ImpactStatement": "Guests who use Microsoft 365 in their organization can sign in using their work or school account to access the site or document. After the one-time passcode for verification has been entered for the first time, guests will authenticate with their work or school account and have a guest account created in the host's organization.", + "RemediationProcedure": "To remediate using the UI: 1. Navigate to SharePoint admin center https://admin.microsoft.com/sharepoint. 2. Click to expand Policies > Sharing. 3. Scroll to and expand More external sharing settings. 4. Set People who use a verification code must reauthenticate after this many days to 15 or less.", + "AuditProcedure": "To audit using the UI: 1. Navigate to SharePoint admin center https://admin.microsoft.com/sharepoint. 2. Click to expand Policies > Sharing. 3. Scroll to and expand More external sharing settings. 4. Ensure People who use a verification code must reauthenticate after this many days is set to 15 or less.", + "AdditionalInformation": "If OneDrive and SharePoint integration with Entra ID B2B is enabled as per the CIS Benchmark the one-time-passcode experience will be replaced.", + "References": "https://learn.microsoft.com/en-us/sharepoint/turn-external-sharing-on-or-off#change-the-organization-level-external-sharing-setting", + "DefaultValue": "EmailAttestationRequired: False, EmailAttestationReAuthDays: 30" + } + ] + }, + { + "Id": "7.2.11", + "Description": "This setting configures the permission that is selected by default for sharing link from a SharePoint site. Ensure the SharePoint default sharing link permission is set to View.", + "Checks": [], + "Attributes": [ + { + "Section": "7 SharePoint admin center", + "SubSection": "7.2 Policies", + "Profile": "E3 Level 1", + "AssessmentStatus": "Automated", + "Description": "This setting configures the permission that is selected by default for sharing link from a SharePoint site. Ensure the SharePoint default sharing link permission is set to View.", + "RationaleStatement": "Setting the view permission as the default ensures that users must deliberately select the edit permission when sharing a link. This approach reduces the risk of unintentionally granting edit privileges to a resource that only requires read access, supporting the principle of least privilege.", + "ImpactStatement": "Not applicable.", + "RemediationProcedure": "To remediate using the UI: 1. Navigate to SharePoint admin center https://admin.microsoft.com/sharepoint. 2. Click to expand Policies > Sharing. 3. Scroll to File and folder links. 4. Set Choose the permission that's selected by default for sharing links to View.", + "AuditProcedure": "To audit using the UI: 1. Navigate to SharePoint admin center https://admin.microsoft.com/sharepoint. 2. Click to expand Policies > Sharing. 3. Scroll to File and folder links. 4. Ensure Choose the permission that's selected by default for sharing links is set to View.", + "AdditionalInformation": "", + "References": "https://learn.microsoft.com/en-us/sharepoint/turn-external-sharing-on-or-off#file-and-folder-links", + "DefaultValue": "DefaultLinkPermission: Edit" + } + ] + }, + { + "Id": "7.3.1", + "Description": "By default, SharePoint online allows files that Defender for Office 365 has detected as infected to be downloaded. Ensure Office 365 SharePoint infected files are disallowed for download.", + "Checks": [], + "Attributes": [ + { + "Section": "7 SharePoint admin center", + "SubSection": "7.3 Settings", + "Profile": "E5 Level 2", + "AssessmentStatus": "Automated", + "Description": "By default, SharePoint online allows files that Defender for Office 365 has detected as infected to be downloaded. Ensure Office 365 SharePoint infected files are disallowed for download.", + "RationaleStatement": "Defender for Office 365 for SharePoint, OneDrive, and Microsoft Teams protects your organization from inadvertently sharing malicious files. When an infected file is detected that file is blocked so that no one can open, copy, move, or share it until further actions are taken by the organization's security team.", + "ImpactStatement": "The only potential impact associated with implementation of this setting is potential inconvenience associated with the small percentage of false positive detections that may occur.", + "RemediationProcedure": "To remediate using PowerShell: 1. Connect to SharePoint Online using Connect-SPOService -Url https://tenant-admin.sharepoint.com, replacing 'tenant' with the appropriate value. 2. Run the following PowerShell command to set the recommended value: Set-SPOTenant -DisallowInfectedFileDownload $true", + "AuditProcedure": "To audit using PowerShell: 1. Connect to SharePoint Online using Connect-SPOService -Url https://tenant-admin.sharepoint.com, replacing 'tenant' with the appropriate value. 2. Run the following PowerShell command: Get-SPOTenant | Select-Object DisallowInfectedFileDownload. 3. Ensure the value for DisallowInfectedFileDownload is set to True.", + "AdditionalInformation": "According to Microsoft, SharePoint cannot be accessed through PowerShell by users with the Global Reader role.", + "References": "https://learn.microsoft.com/en-us/defender-office-365/safe-attachments-for-spo-odfb-teams-configure", + "DefaultValue": "False" + } + ] + }, + { + "Id": "7.3.2", + "Description": "Microsoft OneDrive allows users to sign in their cloud tenant account and begin syncing select folders or the entire contents of OneDrive to a local computer. By default, this includes any computer with OneDrive already installed. Ensure OneDrive sync is restricted for unmanaged devices.", + "Checks": [ + "sharepoint_onedrive_sync_restricted_unmanaged_devices" + ], + "Attributes": [ + { + "Section": "7 SharePoint admin center", + "SubSection": "7.3 Settings", + "Profile": "E3 Level 2", + "AssessmentStatus": "Automated", + "Description": "Microsoft OneDrive allows users to sign in their cloud tenant account and begin syncing select folders or the entire contents of OneDrive to a local computer. By default, this includes any computer with OneDrive already installed. Ensure OneDrive sync is restricted for unmanaged devices by allowing syncing only on computers joined to specific domains.", + "RationaleStatement": "Unmanaged devices pose a risk, since their security cannot be verified through existing security policies, brokers or endpoint protection. Allowing users to sync data to these devices takes that data out of the control of the organization. This increases the risk of the data either being intentionally or accidentally leaked.", + "ImpactStatement": "Enabling this feature will prevent users from using the OneDrive for Business Sync client on devices that are not joined to the domains that were defined.", + "RemediationProcedure": "To remediate using the UI: 1. Navigate to SharePoint admin center https://admin.microsoft.com/sharepoint. 2. Click Settings followed by OneDrive - Sync. 3. Check the Allow syncing only on computers joined to specific domains. 4. Use the Get-ADDomain PowerShell command on the on-premises server to obtain the GUID for each on-premises domain. 5. Click Save.", + "AuditProcedure": "To audit using the UI: 1. Navigate to SharePoint admin center https://admin.microsoft.com/sharepoint. 2. Click Settings followed by OneDrive - Sync. 3. Verify that Allow syncing only on computers joined to specific domains is checked. 4. Verify that the Active Directory domain GUIDS are listed in the box.", + "AdditionalInformation": "This setting is only applicable to Active Directory domains when operating in a hybrid configuration. It does not apply to Entra domains. If there are devices which are only Entra ID joined, consider using a Conditional Access Policy instead.", + "References": "https://learn.microsoft.com/en-us/sharepoint/allow-syncing-only-on-specific-domains", + "DefaultValue": "TenantRestrictionEnabled: False, AllowedDomainList: {}" + } + ] + }, + { + "Id": "8.1.1", + "Description": "Microsoft Teams enables collaboration via file sharing. This file sharing is conducted within Teams, using SharePoint Online, by default; however, third-party cloud services are allowed as well. Ensure external file sharing in Teams is enabled for only approved cloud storage services.", + "Checks": [ + "teams_external_file_sharing_restricted" + ], + "Attributes": [ + { + "Section": "8 Microsoft Teams admin center", + "SubSection": "8.1 Teams", + "Profile": "E3 Level 2", + "AssessmentStatus": "Automated", + "Description": "Microsoft Teams enables collaboration via file sharing. This file sharing is conducted within Teams, using SharePoint Online, by default; however, third-party cloud services are allowed as well. Ensure external file sharing in Teams is enabled for only approved cloud storage services.", + "RationaleStatement": "Ensuring that only authorized cloud storage providers are accessible from Teams will help to dissuade the use of non-approved storage providers.", + "ImpactStatement": "The impact associated with this change is highly dependent upon current practices in the tenant. If users do not use other storage providers, then minimal impact is likely. However, if users do regularly utilize providers outside of the tenant this will affect their ability to continue to do so.", + "RemediationProcedure": "To remediate using the UI: 1. Navigate to Microsoft Teams admin center https://admin.teams.microsoft.com. 2. Select Settings & policies > Global (Org-wide default) settings. 3. Click Teams to open the Teams settings section. 4. Under files set storages providers to Off unless they have first been authorized by the organization. To remediate using PowerShell: 1. Connect to Teams PowerShell using Connect-MicrosoftTeams. 2. Run the following PowerShell command to disable external providers that are not authorized: $Params = @{ Identity = 'Global'; AllowGoogleDrive = $false; AllowShareFile = $false; AllowBox = $false; AllowDropBox = $false; AllowEgnyte = $false }; Set-CsTeamsClientConfiguration @Params", + "AuditProcedure": "To audit using the UI: 1. Navigate to Microsoft Teams admin center https://admin.teams.microsoft.com. 2. Select Settings & policies > Global (Org-wide default) settings. 3. Click Teams to open the Teams settings section. 4. Under files verify that only organizationally authorized cloud storage options are set to On and all others Off. To audit using PowerShell: 1. Connect to Teams PowerShell using Connect-MicrosoftTeams. 2. Run the following to verify the recommended state: $Params = @('AllowDropbox', 'AllowBox', 'AllowGoogleDrive', 'AllowShareFile', 'AllowEgnyte'); Get-CsTeamsClientConfiguration -Identity Global | fl $Params. 3. Verify that only authorized providers are set to True and all others False.", + "AdditionalInformation": "", + "References": "https://learn.microsoft.com/en-us/microsoftteams/teams-powershell-managing-teams", + "DefaultValue": "AllowDropBox: True, AllowBox: True, AllowGoogleDrive: True, AllowShareFile: True, AllowEgnyte: True" + } + ] + }, + { + "Id": "8.1.2", + "Description": "This setting controls whether Teams channels are allowed to receive emails sent to their unique email addresses. When enabled, emails sent to a channel's address will be delivered and appear in the channel's conversation thread; when disabled, the channel will reject incoming emails, preventing them from being posted. Ensure users can't send emails to a channel email address.", + "Checks": [ + "teams_email_sending_to_channel_disabled" + ], + "Attributes": [ + { + "Section": "8 Microsoft Teams admin center", + "SubSection": "8.1 Teams", + "Profile": "E3 Level 1", + "AssessmentStatus": "Automated", + "Description": "This setting controls whether Teams channels are allowed to receive emails sent to their unique email addresses. When enabled, emails sent to a channel's address will be delivered and appear in the channel's conversation thread; when disabled, the channel will reject incoming emails, preventing them from being posted. The recommended state is Off.", + "RationaleStatement": "Channel email addresses are not under the tenant's domain and organizations do not have control over the security settings for this email address. An attacker could email channels directly if they discover the channel email address.", + "ImpactStatement": "Depending on the organization's adoption, disabling this may disrupt workflows that rely on email-to-channel communication, particularly in environments where email is used to bridge external systems or vendors into Teams. This could include reduced visibility of important updates or alerts that were previously routed into Teams channels via email.", + "RemediationProcedure": "To remediate using the UI: 1. Navigate to Microsoft Teams admin center https://admin.teams.microsoft.com. 2. Select Settings & policies > Global (Org-wide default) settings. 3. Click Teams to open the Teams settings section. 4. Under email integration set Users can send emails to a channel email address to Off. To remediate using PowerShell: 1. Connect to Teams PowerShell using Connect-MicrosoftTeams. 2. Run the following command to set the recommended state: Set-CsTeamsClientConfiguration -Identity Global -AllowEmailIntoChannel $false", + "AuditProcedure": "To audit using the UI: 1. Navigate to Microsoft Teams admin center https://admin.teams.microsoft.com. 2. Select Settings & policies > Global (Org-wide default) settings. 3. Click Teams to open the Teams settings section. 4. Under email integration verify that Users can send emails to a channel email address is Off. To audit using PowerShell: 1. Connect to Teams PowerShell using Connect-MicrosoftTeams. 2. Run the following command to verify the recommended state: Get-CsTeamsClientConfiguration -Identity Global | fl AllowEmailIntoChannel. 3. Ensure the returned value is False.", + "AdditionalInformation": "", + "References": "https://learn.microsoft.com/en-us/microsoft-365/security/office-365-security/step-by-step-guides/reducing-attack-surface-in-microsoft-teams?view=o365-worldwide#restricting-channel-email-messages-to-approved-domains", + "DefaultValue": "On (True)" + } + ] + }, + { + "Id": "8.2.1", + "Description": "This policy controls whether external domains are allowed, blocked or permitted based on an allowlist or denylist. When external domains are allowed, users in your organization can chat, add users to meetings, and use audio video conferencing with users in external organizations. Ensure external domains are restricted in the Teams admin center.", + "Checks": [ + "teams_external_domains_restricted" + ], + "Attributes": [ + { + "Section": "8 Microsoft Teams admin center", + "SubSection": "8.2 Users", + "Profile": "E3 Level 2", + "AssessmentStatus": "Automated", + "Description": "This policy controls whether external domains are allowed, blocked or permitted based on an allowlist or denylist. When external domains are allowed, users in your organization can chat, add users to meetings, and use audio video conferencing with users in external organizations. The recommended state is Allow only specific external domains or Block all external domains.", + "RationaleStatement": "Allowlisting external domains that an organization is collaborating with allows for stringent controls over who an organization's users are allowed to make contact with. Some real-world attacks and exploits delivered via Teams over external access channels include: DarkGate malware, Social engineering / Phishing attacks by 'Midnight Blizzard', GIFShell, Username enumeration.", + "ImpactStatement": "The impact in terms of the type of collaboration users are allowed to participate in and the I.T. resources expended to manage an allowlist will increase. If a user attempts to join the inviting organization's meeting they will be prevented from joining unless they were created as a guest in EntraID or their domain was added to the allowed external domains list.", + "RemediationProcedure": "To remediate using the UI: 1. Navigate to Microsoft Teams admin center https://admin.teams.microsoft.com/. 2. Click to expand Users select External access. 3. Select the Policies tab. 4. Click on the Global (Org-wide default) policy. 5. Set Teams and Skype for Business users in external organizations to Off. 6. Click Save. To remediate using PowerShell: 1. Connect to Teams PowerShell using Connect-MicrosoftTeams. 2. Run the following command to configure the Global (Org-wide default) policy: Set-CsExternalAccessPolicy -Identity Global -EnableFederationAccess $false", + "AuditProcedure": "To audit using the UI: 1. Navigate to Microsoft Teams admin center https://admin.teams.microsoft.com/. 2. Click to expand Users select External access. 3. Select the Policies tab. 4. Click on the Global (Org-wide default) policy. 5. Ensure Teams and Skype for Business users in external organizations is set to Off. To audit using PowerShell: 1. Connect to Teams PowerShell using Connect-MicrosoftTeams. 2. Run the following command: Get-CsExternalAccessPolicy -Identity Global. 3. Ensure EnableFederationAccess is False.", + "AdditionalInformation": "The organization settings take precedence over the policy settings. The audit is considered satisfied if the organizational setting is configured as prescribed, regardless of whether the Global default policy value is True or False.", + "References": "https://learn.microsoft.com/en-us/microsoftteams/trusted-organizations-external-meetings-chat?tabs=organization-settings", + "DefaultValue": "EnableFederationAccess: True" + } + ] + }, + { + "Id": "8.2.2", + "Description": "This policy setting controls chats and meetings with external unmanaged Teams users (those not managed by an organization, such as Microsoft Teams (free)). Ensure communication with unmanaged Teams users is disabled.", + "Checks": [ + "teams_unmanaged_communication_disabled" + ], + "Attributes": [ + { + "Section": "8 Microsoft Teams admin center", + "SubSection": "8.2 Users", + "Profile": "E3 Level 1", + "AssessmentStatus": "Automated", + "Description": "This policy setting controls chats and meetings with external unmanaged Teams users (those not managed by an organization, such as Microsoft Teams (free)). The recommended state is: People in my organization can communicate with unmanaged Teams accounts set to Off.", + "RationaleStatement": "Allowing users to communicate with unmanaged Teams users presents a potential security threat as little effort is required by threat actors to gain access to a trial or free Microsoft Teams account. Some real-world attacks and exploits delivered via Teams over external access channels include: DarkGate malware, Social engineering / Phishing attacks by 'Midnight Blizzard', GIFShell, Username enumeration.", + "ImpactStatement": "Users will be unable to communicate with Teams users who are not managed by an organization. Organizations may choose create additional policies for specific groups needing to communicating with unmanaged external users.", + "RemediationProcedure": "To remediate using the UI: 1. Navigate to Microsoft Teams admin center https://admin.teams.microsoft.com/. 2. Click to expand Users select External access. 3. Select the Policies tab. 4. Click on the Global (Org-wide default) policy. 5. Set People in my organization can communicate with unmanaged Teams accounts to Off. 6. Click Save. To remediate using PowerShell: 1. Connect to Teams PowerShell using Connect-MicrosoftTeams. 2. Run the following command: Set-CsExternalAccessPolicy -Identity Global -EnableTeamsConsumerAccess $false", + "AuditProcedure": "To audit using the UI: 1. Navigate to Microsoft Teams admin center https://admin.teams.microsoft.com/. 2. Click to expand Users select External access. 3. Select the Policies tab. 4. Click on the Global (Org-wide default) policy. 5. Ensure People in my organization can communicate with unmanaged Teams accounts is set to Off. To audit using PowerShell: 1. Connect to Teams PowerShell using Connect-MicrosoftTeams. 2. Run the following command: Get-CsExternalAccessPolicy -Identity Global. Ensure EnableTeamsConsumerAccess is set to False.", + "AdditionalInformation": "The settings that govern chats and meetings with external unmanaged Teams users aren't available in GCC, GCC High, or DOD deployments, or in private cloud environments.", + "References": "https://learn.microsoft.com/en-us/microsoftteams/trusted-organizations-external-meetings-chat?tabs=organization-settings", + "DefaultValue": "EnableTeamsConsumerAccess: True" + } + ] + }, + { + "Id": "8.2.3", + "Description": "This setting prevents external users who are not managed by an organization from initiating contact with users in the protected organization. Ensure external Teams users cannot initiate conversations.", + "Checks": [ + "teams_external_users_cannot_start_conversations" + ], + "Attributes": [ + { + "Section": "8 Microsoft Teams admin center", + "SubSection": "8.2 Users", + "Profile": "E3 Level 1", + "AssessmentStatus": "Automated", + "Description": "This setting prevents external users who are not managed by an organization from initiating contact with users in the protected organization. The recommended state is to uncheck External users with Teams accounts not managed by an organization can contact users in my organization.", + "RationaleStatement": "Allowing users to communicate with unmanaged Teams users presents a potential security threat as little effort is required by threat actors to gain access to a trial or free Microsoft Teams account. Some real-world attacks and exploits delivered via Teams over external access channels include: DarkGate malware, Social engineering / Phishing attacks by 'Midnight Blizzard', GIFShell, Username enumeration.", + "ImpactStatement": "The impact of disabling this is very low. Organizations may choose to create additional policies for specific groups that need to communicate with unmanaged external users.", + "RemediationProcedure": "To remediate using the UI: 1. Navigate to Microsoft Teams admin center https://admin.teams.microsoft.com/. 2. Click to expand Users select External access. 3. Select the Policies tab. 4. Click on the Global (Org-wide default) policy. 5. Locate the parent setting People in my organization can communicate with unmanaged Teams accounts. 6. Uncheck External users with Teams accounts not managed by an organization can contact users in my organization. 7. Click Save. To remediate using PowerShell: 1. Connect to Teams PowerShell using Connect-MicrosoftTeams. 2. Run the following command: Set-CsExternalAccessPolicy -Identity Global -EnableTeamsConsumerInbound $false", + "AuditProcedure": "To audit using the UI: 1. Navigate to Microsoft Teams admin center https://admin.teams.microsoft.com/. 2. Click to expand Users select External access. 3. Select the Policies tab. 4. Click on the Global (Org-wide default) policy. 5. Ensure External users with Teams accounts not managed by an organization can contact users in my organization is not checked (false). To audit using PowerShell: 1. Connect to Teams PowerShell using Connect-MicrosoftTeams. 2. Run the following command: Get-CsExternalAccessPolicy -Identity Global. Ensure EnableTeamsConsumerInbound is False.", + "AdditionalInformation": "Chats and meetings with external unmanaged Teams users isn't available in GCC, GCC High, or DOD deployments, or in private cloud environments.", + "References": "https://learn.microsoft.com/en-us/microsoftteams/trusted-organizations-external-meetings-chat?tabs=organization-settings", + "DefaultValue": "EnableTeamsConsumerInbound: True" + } + ] + }, + { + "Id": "8.2.4", + "Description": "This setting controls the organization's external access with Teams 'trial-only' tenants. These are tenants that don't have any purchased seats. Ensure the organization cannot communicate with accounts in trial Teams tenants.", + "Checks": [], + "Attributes": [ + { + "Section": "8 Microsoft Teams admin center", + "SubSection": "8.2 Users", + "Profile": "E3 Level 1", + "AssessmentStatus": "Automated", + "Description": "This setting controls the organization's external access with Teams 'trial-only' tenants. These are tenants that don't have any purchased seats. When set to Blocked, users from these trial-only tenants aren't able to search and contact your users via chats, Teams calls, and meetings (using the users' authenticated identities) and your users aren't able to reach users in these trial-only tenants. The recommended state for People in my organization can communicate with accounts in trial Teams tenant is Off.", + "RationaleStatement": "Microsoft introduced this setting as Off by default on July 29, 2029 in order to block attack vectors being exploited by threat actors who have abused trial tenants. Enforcing the default ensures the setting is not reenabled for any reason. Allowing users to communicate with unmanaged Teams users presents a potential security threat as little effort is required by threat actors to gain access to a trial or free Microsoft Teams account.", + "ImpactStatement": "There is minimal to no legitimate business need for users to communicate with accounts in trial tenants. For temporary or testing scenarios, alternative communication methods are readily available that do not require enabling this setting.", + "RemediationProcedure": "To remediate using the UI: 1. Navigate to Microsoft Teams admin center https://admin.teams.microsoft.com/. 2. Click to expand Users select External access. 3. Select the Organization settings tab. 4. Set People in my organization can communicate with accounts in trial Teams tenant to Off. To remediate using PowerShell: 1. Connect to Teams PowerShell using Connect-MicrosoftTeams. 2. Run the following command: Set-CsTenantFederationConfiguration -ExternalAccessWithTrialTenants 'Blocked'", + "AuditProcedure": "To audit using the UI: 1. Navigate to Microsoft Teams admin center https://admin.teams.microsoft.com/. 2. Click to expand Users select External access. 3. Select the Organization settings tab. 4. Ensure People in my organization can communicate with accounts in trial Teams tenant is set to Off. To audit using PowerShell: 1. Connect to Teams PowerShell using Connect-MicrosoftTeams. 2. Run the following command: Get-CsTenantFederationConfiguration. Ensure ExternalAccessWithTrialTenants is set to Blocked.", + "AdditionalInformation": "", + "References": "https://learn.microsoft.com/en-us/microsoftteams/trusted-organizations-external-meetings-chat?tabs=organization-settings#block-federation-with-teams-trial-only-tenants", + "DefaultValue": "Off or Blocked" + } + ] + }, + { + "Id": "8.4.1", + "Description": "This policy setting controls which class of apps are available for users to install. Ensure app permission policies are configured to restrict third-party and custom apps.", + "Checks": [], + "Attributes": [ + { + "Section": "8 Microsoft Teams admin center", + "SubSection": "8.4 Teams apps", + "Profile": "E3 Level 1", + "AssessmentStatus": "Manual", + "Description": "This policy setting controls which class of apps are available for users to install.", + "RationaleStatement": "Allowing users to install third-party or unverified apps poses a potential risk of introducing malicious software to the environment.", + "ImpactStatement": "Users will only be able to install approved classes of apps.", + "RemediationProcedure": "To remediate using the UI: 1. Navigate to Microsoft Teams admin center https://admin.teams.microsoft.com. 2. Click to expand Teams apps select Manage apps. 3. In the upper right click Actions > Org-wide app settings. 4. For Microsoft apps set Let users install and use available apps by default to On or less permissive. 5. For Third-party apps set Let users install and use available apps by default to Off. 6. For Custom apps set Let users install and use available apps by default to Off. 7. For Custom apps set Let users interact with custom apps in preview to Off.", + "AuditProcedure": "To audit using the UI: 1. Navigate to Microsoft Teams admin center https://admin.teams.microsoft.com. 2. Click to expand Teams apps select Manage apps. 3. In the upper right click Actions > Org-wide app settings. 4. For Microsoft apps verify that Let users install and use available apps by default is On or less permissive. 5. For Third-party apps verify Let users install and use available apps by default is Off. 6. For Custom apps verify Let users install and use available apps by default is Off. 7. For Custom apps verify Let users interact with custom apps in preview is Off.", + "AdditionalInformation": "", + "References": "https://learn.microsoft.com/en-us/microsoftteams/app-centric-management", + "DefaultValue": "Microsoft apps: On, Third-party apps: On, Custom apps: On" + } + ] + }, + { + "Id": "8.5.1", + "Description": "Anonymous users are users whose identity can't be verified. They may be logged in to an organization without a mutual trust relationship or they may not have an account (guest or user). Ensure anonymous users can't join a meeting.", + "Checks": [ + "teams_meeting_anonymous_user_join_disabled" + ], + "Attributes": [ + { + "Section": "8 Microsoft Teams admin center", + "SubSection": "8.5 Meetings", + "Profile": "E3 Level 2", + "AssessmentStatus": "Automated", + "Description": "Anonymous users are users whose identity can't be verified. They may be logged in to an organization without a mutual trust relationship or they may not have an account (guest or user). Anonymous participants appear with '(Unverified)' appended to their name in meetings. The recommended state is Anonymous users can join a meeting unverified set to Off.", + "RationaleStatement": "For meetings that could contain sensitive information, it is best to allow the meeting organizer to vet anyone not directly sent an invite before admitting them to the meeting. This will also prevent the anonymous user from using the meeting link to have meetings at unscheduled times.", + "ImpactStatement": "Individuals who were not sent or forwarded a meeting invite will not be able to join the meeting automatically.", + "RemediationProcedure": "To remediate using the UI: 1. Navigate to Microsoft Teams admin center https://admin.teams.microsoft.com. 2. Select Settings & policies > Global (Org-wide default) settings. 3. Select Meetings to open the meeting settings section. 4. Under meeting join & lobby set Anonymous users can join a meeting unverified to Off. To remediate using PowerShell: 1. Connect to Teams PowerShell using Connect-MicrosoftTeams. 2. Run the following command to set the recommended state: Set-CsTeamsMeetingPolicy -Identity Global -AllowAnonymousUsersToJoinMeeting $false", + "AuditProcedure": "To audit using the UI: 1. Navigate to Microsoft Teams admin center https://admin.teams.microsoft.com. 2. Select Settings & policies > Global (Org-wide default) settings. 3. Select Meetings to open the meeting settings section. 4. Under meeting join & lobby verify that Anonymous users can join a meeting unverified is set to Off. To audit using PowerShell: 1. Connect to Teams PowerShell using Connect-MicrosoftTeams. 2. Run the following command to verify the recommended state: Get-CsTeamsMeetingPolicy -Identity Global | fl AllowAnonymousUsersToJoinMeeting. 3. Ensure the returned value is False.", + "AdditionalInformation": "", + "References": "https://learn.microsoft.com/en-us/defender-office-365/step-by-step-guides/reducing-attack-surface-in-microsoft-teams?view=o365-worldwide#configure-meeting-settings", + "DefaultValue": "On (True)" + } + ] + }, + { + "Id": "8.5.2", + "Description": "This policy setting controls if an anonymous participant can start a Microsoft Teams meeting without someone in attendance. Anonymous users and dial-in callers must wait in the lobby until the meeting is started by someone in the organization or an external user from a trusted organization. Ensure anonymous users and dial-in callers can't start a meeting.", + "Checks": [ + "teams_meeting_anonymous_user_start_disabled" + ], + "Attributes": [ + { + "Section": "8 Microsoft Teams admin center", + "SubSection": "8.5 Meetings", + "Profile": "E3 Level 1", + "AssessmentStatus": "Automated", + "Description": "This policy setting controls if an anonymous participant can start a Microsoft Teams meeting without someone in attendance. Anonymous users and dial-in callers must wait in the lobby until the meeting is started by someone in the organization or an external user from a trusted organization.", + "RationaleStatement": "Not allowing anonymous participants to automatically join a meeting reduces the risk of meeting spamming.", + "ImpactStatement": "Anonymous participants will not be able to start a Microsoft Teams meeting.", + "RemediationProcedure": "To remediate using the UI: 1. Navigate to Microsoft Teams admin center https://admin.teams.microsoft.com. 2. Select Settings & policies > Global (Org-wide default) settings. 3. Select Meetings to open the meeting settings section. 4. Under meeting join & lobby set Anonymous users and dial-in callers can start a meeting to Off. To remediate using PowerShell: 1. Connect to Teams PowerShell using Connect-MicrosoftTeams. 2. Run the following command to set the recommended state: Set-CsTeamsMeetingPolicy -Identity Global -AllowAnonymousUsersToStartMeeting $false", + "AuditProcedure": "To audit using the UI: 1. Navigate to Microsoft Teams admin center https://admin.teams.microsoft.com. 2. Select Settings & policies > Global (Org-wide default) settings. 3. Select Meetings to open the meeting settings section. 4. Under meeting join & lobby verify that Anonymous users and dial-in callers can start a meeting is set to Off. To audit using PowerShell: 1. Connect to Teams PowerShell using Connect-MicrosoftTeams. 2. Run the following command to verify the recommended state: Get-CsTeamsMeetingPolicy -Identity Global | fl AllowAnonymousUsersToStartMeeting. 3. Ensure the returned value is False.", + "AdditionalInformation": "This setting only applies when Who can bypass the lobby is set to Everyone. If the anonymous users can join a meeting organization-level setting or meeting policy is Off, this setting only applies to dial-in callers.", + "References": "https://learn.microsoft.com/en-us/microsoftteams/anonymous-users-in-meetings", + "DefaultValue": "Off (False)" + } + ] + }, + { + "Id": "8.5.3", + "Description": "This policy setting controls who can join a meeting directly and who must wait in the lobby until they're admitted by an organizer, co-organizer, or presenter of the meeting. Ensure only people in my org can bypass the lobby.", + "Checks": [ + "teams_meeting_external_lobby_bypass_disabled" + ], + "Attributes": [ + { + "Section": "8 Microsoft Teams admin center", + "SubSection": "8.5 Meetings", + "Profile": "E3 Level 1", + "AssessmentStatus": "Automated", + "Description": "This policy setting controls who can join a meeting directly and who must wait in the lobby until they're admitted by an organizer, co-organizer, or presenter of the meeting. The recommended state is People who were invited or more restrictive.", + "RationaleStatement": "For meetings that could contain sensitive information, it is best to allow the meeting organizer to vet anyone not directly sent an invite before admitting them to the meeting. This will also prevent the anonymous user from using the meeting link to have meetings at unscheduled times.", + "ImpactStatement": "Individuals who are not part of the organization will have to wait in the lobby until they're admitted by an organizer, co-organizer, or presenter of the meeting. Any individual who dials into the meeting regardless of status will also have to wait in the lobby. This includes internal users who are considered unauthenticated when dialing in.", + "RemediationProcedure": "To remediate using the UI: 1. Navigate to Microsoft Teams admin center https://admin.teams.microsoft.com. 2. Select Settings & policies > Global (Org-wide default) settings. 3. Select Meetings to open the meeting settings section. 4. Under meeting join & lobby set Who can bypass the lobby to People who were invited or a more restrictive value: People in my org, Only organizers and co-organizers. To remediate using PowerShell: 1. Connect to Teams PowerShell using Connect-MicrosoftTeams. 2. Run the following command to set the recommended state: Set-CsTeamsMeetingPolicy -Identity Global -AutoAdmittedUsers 'InvitedUsers'", + "AuditProcedure": "To audit using the UI: 1. Navigate to Microsoft Teams admin center https://admin.teams.microsoft.com. 2. Select Settings & policies > Global (Org-wide default) settings. 3. Select Meetings to open the meeting settings section. 4. Under meeting join & lobby verify Who can bypass the lobby is set to People who were invited or a more restrictive value: People in my org, Only organizers and co-organizers. To audit using PowerShell: 1. Connect to Teams PowerShell using Connect-MicrosoftTeams. 2. Run the following command to verify the recommended state: Get-CsTeamsMeetingPolicy -Identity Global | fl AutoAdmittedUsers. 3. Ensure the returned value is InvitedUsers or more restrictive: EveryoneInCompanyExcludingGuests, OrganizerOnly.", + "AdditionalInformation": "", + "References": "https://learn.microsoft.com/en-us/microsoftteams/who-can-bypass-meeting-lobby#overview-of-lobby-settings-and-policies", + "DefaultValue": "People in my org and guests (EveryoneInCompany)" + } + ] + }, + { + "Id": "8.5.4", + "Description": "This policy setting controls if users who dial in by phone can join the meeting directly or must wait in the lobby. Admittance to the meeting from the lobby is authorized by the meeting organizer, co-organizer, or presenter of the meeting. Ensure users dialing in can't bypass the lobby.", + "Checks": [ + "teams_meeting_dial_in_lobby_bypass_disabled" + ], + "Attributes": [ + { + "Section": "8 Microsoft Teams admin center", + "SubSection": "8.5 Meetings", + "Profile": "E3 Level 1", + "AssessmentStatus": "Automated", + "Description": "This policy setting controls if users who dial in by phone can join the meeting directly or must wait in the lobby. Admittance to the meeting from the lobby is authorized by the meeting organizer, co-organizer, or presenter of the meeting.", + "RationaleStatement": "For meetings that could contain sensitive information, it is best to allow the meeting organizer to vet anyone not directly from the organization.", + "ImpactStatement": "Individuals who are dialing in to the meeting must wait in the lobby until a meeting organizer, co-organizer, or presenter admits them.", + "RemediationProcedure": "To remediate using the UI: 1. Navigate to Microsoft Teams admin center https://admin.teams.microsoft.com. 2. Select Settings & policies > Global (Org-wide default) settings. 3. Select Meetings to open the meeting settings section. 4. Under meeting join & lobby set People dialing in can bypass the lobby to Off. To remediate using PowerShell: 1. Connect to Teams PowerShell using Connect-MicrosoftTeams. 2. Run the following command to set the recommended state: Set-CsTeamsMeetingPolicy -Identity Global -AllowPSTNUsersToBypassLobby $false", + "AuditProcedure": "To audit using the UI: 1. Navigate to Microsoft Teams admin center https://admin.teams.microsoft.com. 2. Select Settings & policies > Global (Org-wide default) settings. 3. Select Meetings to open the meeting settings section. 4. Under meeting join & lobby verify that People dialing in can bypass the lobby is set to Off. To audit using PowerShell: 1. Connect to Teams PowerShell using Connect-MicrosoftTeams. 2. Run the following command to verify the recommended state: Get-CsTeamsMeetingPolicy -Identity Global | fl AllowPSTNUsersToBypassLobby. 3. Ensure the value is False.", + "AdditionalInformation": "", + "References": "https://learn.microsoft.com/en-us/microsoftteams/who-can-bypass-meeting-lobby#overview-of-lobby-settings-and-policies", + "DefaultValue": "Off (False)" + } + ] + }, + { + "Id": "8.5.5", + "Description": "This policy setting controls who has access to read and write chat messages during a meeting. Ensure meeting chat does not allow anonymous users.", + "Checks": [ + "teams_meeting_chat_anonymous_users_disabled" + ], + "Attributes": [ + { + "Section": "8 Microsoft Teams admin center", + "SubSection": "8.5 Meetings", + "Profile": "E3 Level 2", + "AssessmentStatus": "Automated", + "Description": "This policy setting controls who has access to read and write chat messages during a meeting.", + "RationaleStatement": "Ensuring that only authorized individuals can read and write chat messages during a meeting reduces the risk that a malicious user can inadvertently show content that is not appropriate or view sensitive information.", + "ImpactStatement": "Only authorized individuals will be able to read and write chat messages during a meeting.", + "RemediationProcedure": "To remediate using the UI: 1. Navigate to Microsoft Teams admin center https://admin.teams.microsoft.com. 2. Select Settings & policies > Global (Org-wide default) settings. 3. Select Meetings to open the meeting settings section. 4. Under meeting engagement set Meeting chat to On for everyone but anonymous users. To remediate using PowerShell: 1. Connect to Teams PowerShell using Connect-MicrosoftTeams. 2. Run the following command to set the minimum recommended state: Set-CsTeamsMeetingPolicy -Identity Global -MeetingChatEnabledType 'EnabledExceptAnonymous'", + "AuditProcedure": "To audit using the UI: 1. Navigate to Microsoft Teams admin center https://admin.teams.microsoft.com. 2. Select Settings & policies > Global (Org-wide default) settings. 3. Select Meetings to open the meeting settings section. 4. Under meeting engagement verify that Meeting chat is set to On for everyone but anonymous users or a more restrictive value: In-meeting only except anonymous or Off. To audit using PowerShell: 1. Connect to Teams PowerShell using Connect-MicrosoftTeams. 2. Run the following command to verify the recommended state: Get-CsTeamsMeetingPolicy -Identity Global | fl MeetingChatEnabledType. 3. Ensure the returned value is EnabledExceptAnonymous or a more restrictive value EnabledInMeetingOnlyForAllExceptAnonymous or Disabled.", + "AdditionalInformation": "", + "References": "https://learn.microsoft.com/en-us/powershell/module/skype/set-csteamsmeetingpolicy?view=skype-ps#-meetingchatenabledtype", + "DefaultValue": "On for everyone (Enabled)" + } + ] + }, + { + "Id": "8.5.6", + "Description": "This policy setting controls who can present in a Teams meeting. Ensure only organizers and co-organizers can present.", + "Checks": [ + "teams_meeting_presenters_restricted" + ], + "Attributes": [ + { + "Section": "8 Microsoft Teams admin center", + "SubSection": "8.5 Meetings", + "Profile": "E3 Level 2", + "AssessmentStatus": "Automated", + "Description": "This policy setting controls who can present in a Teams meeting. Note: Organizers and co-organizers can change this setting when the meeting is set up.", + "RationaleStatement": "Ensuring that only authorized individuals are able to present reduces the risk that a malicious user can inadvertently show content that is not appropriate.", + "ImpactStatement": "Only organizers and co-organizers will be able to present without being granted permission.", + "RemediationProcedure": "To remediate using the UI: 1. Navigate to Microsoft Teams admin center https://admin.teams.microsoft.com. 2. Select Settings & policies > Global (Org-wide default) settings. 3. Select Meetings to open the meeting settings section. 4. Under content sharing set Who can present to Only organizers and co-organizers. To remediate using PowerShell: 1. Connect to Teams PowerShell using Connect-MicrosoftTeams. 2. Run the following command to set the recommended state: Set-CsTeamsMeetingPolicy -Identity Global -DesignatedPresenterRoleMode 'OrganizerOnlyUserOverride'", + "AuditProcedure": "To audit using the UI: 1. Navigate to Microsoft Teams admin center https://admin.teams.microsoft.com. 2. Select Settings & policies > Global (Org-wide default) settings. 3. Select Meetings to open the meeting settings section. 4. Under content sharing verify Who can present is set to Only organizers and co-organizers. To audit using PowerShell: 1. Connect to Teams PowerShell using Connect-MicrosoftTeams. 2. Run the following command to verify the recommended state: Get-CsTeamsMeetingPolicy -Identity Global | fl DesignatedPresenterRoleMode. 3. Ensure the returned value is OrganizerOnlyUserOverride.", + "AdditionalInformation": "", + "References": "https://learn.microsoft.com/en-US/microsoftteams/meeting-who-present-request-control", + "DefaultValue": "Everyone (EveryoneUserOverride)" + } + ] + }, + { + "Id": "8.5.7", + "Description": "This policy setting allows control of who can present in meetings and who can request control of the presentation while a meeting is underway. Ensure external participants can't give or request control.", + "Checks": [ + "teams_meeting_external_control_disabled" + ], + "Attributes": [ + { + "Section": "8 Microsoft Teams admin center", + "SubSection": "8.5 Meetings", + "Profile": "E3 Level 1", + "AssessmentStatus": "Automated", + "Description": "This policy setting allows control of who can present in meetings and who can request control of the presentation while a meeting is underway.", + "RationaleStatement": "Ensuring that only authorized individuals and not external participants are able to present and request control reduces the risk that a malicious user can inadvertently show content that is not appropriate. External participants are categorized as follows: external users, guests, and anonymous users.", + "ImpactStatement": "External participants will not be able to present or request control during the meeting. Warning: This setting also affects webinars.", + "RemediationProcedure": "To remediate using the UI: 1. Navigate to Microsoft Teams admin center https://admin.teams.microsoft.com. 2. Select Settings & policies > Global (Org-wide default) settings. 3. Select Meetings to open the meeting settings section. 4. Under content sharing set External participants can give or request control to Off. To remediate using PowerShell: 1. Connect to Teams PowerShell using Connect-MicrosoftTeams. 2. Run the following command to set the recommended state: Set-CsTeamsMeetingPolicy -Identity Global -AllowExternalParticipantGiveRequestControl $false", + "AuditProcedure": "To audit using the UI: 1. Navigate to Microsoft Teams admin center https://admin.teams.microsoft.com. 2. Select Settings & policies > Global (Org-wide default) settings. 3. Select Meetings to open the meeting settings section. 4. Under content sharing verify that External participants can give or request control is Off. To audit using PowerShell: 1. Connect to Teams PowerShell using Connect-MicrosoftTeams. 2. Run the following command to verify the recommended state: Get-CsTeamsMeetingPolicy -Identity Global | fl AllowExternalParticipantGiveRequestControl. 3. Ensure the returned value is False.", + "AdditionalInformation": "At this time, to give and take control of shared content during a meeting, both parties must be using the Teams desktop client. Control isn't supported when either party is running Teams in a browser.", + "References": "https://learn.microsoft.com/en-us/microsoftteams/meeting-who-present-request-control", + "DefaultValue": "Off (False)" + } + ] + }, + { + "Id": "8.5.8", + "Description": "This meeting policy setting controls whether users can read or write messages in external meeting chats with untrusted organizations. If an external organization is on the list of trusted organizations this setting will be ignored. Ensure external meeting chat is off.", + "Checks": [ + "teams_meeting_external_chat_disabled" + ], + "Attributes": [ + { + "Section": "8 Microsoft Teams admin center", + "SubSection": "8.5 Meetings", + "Profile": "E3 Level 2", + "AssessmentStatus": "Automated", + "Description": "This meeting policy setting controls whether users can read or write messages in external meeting chats with untrusted organizations. If an external organization is on the list of trusted organizations this setting will be ignored.", + "RationaleStatement": "Restricting access to chat in meetings hosted by external organizations limits the opportunity for an exploit like GIFShell or DarkGate malware from being delivered to users.", + "ImpactStatement": "When joining external meetings users will be unable to read or write chat messages in Teams meetings with organizations that they don't have a trust relationship with. This will completely remove the chat functionality in meetings. From an I.T. perspective both the upkeep of adding new organizations to the trusted list and the decision-making process behind whether to trust or not trust an external partner will increase time expenditure.", + "RemediationProcedure": "To remediate using the UI: 1. Navigate to Microsoft Teams admin center https://admin.teams.microsoft.com. 2. Select Settings & policies > Global (Org-wide default) settings. 3. Select Meetings to open the meeting settings section. 4. Under meeting engagement set External meeting chat to Off. To remediate using PowerShell: 1. Connect to Teams PowerShell using Connect-MicrosoftTeams. 2. Run the following command to set the recommended state: Set-CsTeamsMeetingPolicy -Identity Global -AllowExternalNonTrustedMeetingChat $false", + "AuditProcedure": "To audit using the UI: 1. Navigate to Microsoft Teams admin center https://admin.teams.microsoft.com. 2. Select Settings & policies > Global (Org-wide default) settings. 3. Select Meetings to open the meeting settings section. 4. Under meeting engagement verify that External meeting chat is set to Off. To audit using PowerShell: 1. Connect to Teams PowerShell using Connect-MicrosoftTeams. 2. Run the following command to verify the recommended state: Get-CsTeamsMeetingPolicy -Identity Global | fl AllowExternalNonTrustedMeetingChat. 3. Ensure the returned value is False.", + "AdditionalInformation": "", + "References": "https://learn.microsoft.com/en-us/microsoftteams/settings-policies-reference#meeting-engagement", + "DefaultValue": "On (True)" + } + ] + }, + { + "Id": "8.5.9", + "Description": "This setting controls the ability for a user to initiate a recording of a meeting in progress. Ensure meeting recording is off by default.", + "Checks": [ + "teams_meeting_recording_disabled" + ], + "Attributes": [ + { + "Section": "8 Microsoft Teams admin center", + "SubSection": "8.5 Meetings", + "Profile": "E3 Level 2", + "AssessmentStatus": "Automated", + "Description": "This setting controls the ability for a user to initiate a recording of a meeting in progress. The recommended state is Off for the Global (Org-wide default) meeting policy.", + "RationaleStatement": "Disabling meeting recordings in the Global meeting policy ensures that only authorized users, such as organizers, co-organizers, and leads, can initiate a recording. This measure helps safeguard sensitive information by preventing unauthorized individuals from capturing and potentially sharing meeting content. Restricting recording capabilities to specific roles allows organizations to exercise greater control over what is recorded, aligning it with the meeting's confidentiality requirements.", + "ImpactStatement": "If there are no additional policies allowing anyone to record, then recording will effectively be disabled.", + "RemediationProcedure": "To remediate using the UI: 1. Navigate to Microsoft Teams admin center https://admin.teams.microsoft.com. 2. Select Settings & policies > Global (Org-wide default) settings. 3. Select Meetings to open the meeting settings section. 4. Under Recording & transcription set Meeting recording to Off. To remediate using PowerShell: 1. Connect to Teams PowerShell using Connect-MicrosoftTeams. 2. Run the following command to set the recommended state: Set-CsTeamsMeetingPolicy -Identity Global -AllowCloudRecording $false", + "AuditProcedure": "To audit using the UI: 1. Navigate to Microsoft Teams admin center https://admin.teams.microsoft.com. 2. Select Settings & policies > Global (Org-wide default) settings. 3. Select Meetings to open the meeting settings section. 4. Under Recording & transcription verify that Meeting recording is set to Off. To audit using PowerShell: 1. Connect to Teams PowerShell using Connect-MicrosoftTeams. 2. Run the following command to verify the recommended state: Get-CsTeamsMeetingPolicy -Identity Global | fl AllowCloudRecording. 3. Ensure the returned value is False.", + "AdditionalInformation": "Creating a separate policy for users or groups who are allowed to record is expected and in compliance. This control is only for the default meeting policy.", + "References": "https://learn.microsoft.com/en-us/microsoftteams/settings-policies-reference#recording--transcription", + "DefaultValue": "On (True)" + } + ] + }, + { + "Id": "8.6.1", + "Description": "User reporting settings allow a user to report a message as malicious for further analysis. Ensure users can report security concerns in Teams.", + "Checks": [ + "teams_security_reporting_enabled", + "defender_chat_report_policy_configured" + ], + "Attributes": [ + { + "Section": "8 Microsoft Teams admin center", + "SubSection": "8.6 Messaging", + "Profile": "E3 Level 1", + "AssessmentStatus": "Automated", + "Description": "User reporting settings allow a user to report a message as malicious for further analysis. This recommendation is composed of 3 different settings and all must be configured to pass: In the Teams admin center: On by default and controls whether users are able to report messages from Teams. In the Microsoft 365 Defender portal: On by default for new tenants. Defender - Report message destinations: This applies to more than just Microsoft Teams and allows for an organization to keep their reports contained.", + "RationaleStatement": "Users will be able to more quickly and systematically alert administrators of suspicious malicious messages within Teams. The content of these messages may be sensitive in nature and therefore should be kept within the organization and not shared with Microsoft without first consulting company policy.", + "ImpactStatement": "Enabling message reporting has an impact beyond just addressing security concerns. When users of the platform report a message, the content could include messages that are threatening or harassing in nature, possibly stemming from colleagues. Due to this the security staff responsible for reviewing and acting on these reports should be equipped with the skills to discern and appropriately direct such messages to the relevant departments, such as Human Resources (HR).", + "RemediationProcedure": "To remediate using the UI: 1. Navigate to Microsoft Teams admin center https://admin.teams.microsoft.com. 2. Select Settings & policies > Global (Org-wide default) settings. 3. Select Messaging to open the messaging settings section. 4. Set Report a security concern to On. 5. Next, navigate to Microsoft 365 Defender https://security.microsoft.com/. 6. Click on Settings > Email & collaboration > User reported settings. 7. Scroll to Microsoft Teams. 8. Check Monitor reported messages in Microsoft Teams and Save. 9. Set Send reported messages to: to My reporting mailbox only with reports configured to be sent to authorized staff.", + "AuditProcedure": "To audit using the UI: 1. Navigate to Microsoft Teams admin center https://admin.teams.microsoft.com. 2. Select Settings & policies > Global (Org-wide default) settings. 3. Select Messaging to open the messaging settings section. 4. Ensure Report a security concern is On. 5. Next, navigate to Microsoft 365 Defender https://security.microsoft.com/. 6. Click on Settings > Email & collaboration > User reported settings. 7. Scroll to Microsoft Teams. 8. Ensure Monitor reported messages in Microsoft Teams is checked. 9. Ensure Send reported messages to: is set to My reporting mailbox only with report email addresses defined for authorized staff. To audit using PowerShell: 1. Connect to Teams PowerShell using Connect-MicrosoftTeams. 2. Run: Get-CsTeamsMessagingPolicy -Identity Global | fl AllowSecurityEndUserReporting. 3. Ensure the value returned is True. 4. Connect to Exchange Online PowerShell using Connect-ExchangeOnline. 5. Run: Get-ReportSubmissionPolicy | fl Report*. 6. Ensure ReportJunkToCustomizedAddress, ReportNotJunkToCustomizedAddress, ReportPhishToCustomizedAddress are True and ReportChatMessageEnabled is False, ReportChatMessageToCustomizedAddressEnabled is True.", + "AdditionalInformation": "The reported message remains visible to the user in the Teams client. Users can report the same message multiple times. The message sender isn't notified that messages were reported.", + "References": "https://learn.microsoft.com/en-us/defender-office-365/submissions-teams?view=o365-worldwide", + "DefaultValue": "On (True), Report message destination: Microsoft Only" + } + ] + }, + { + "Id": "9.1.1", + "Description": "This setting allows business-to-business (B2B) guests access to Microsoft Fabric, and contents that they have permissions to. With the setting turned off, B2B guest users receive an error when trying to access Power BI. Ensure guest user access is restricted.", + "Checks": [], + "Attributes": [ + { + "Section": "9 Microsoft Fabric", + "SubSection": "9.1 Tenant settings", + "Profile": "E3 Level 1", + "AssessmentStatus": "Manual", + "Description": "This setting allows business-to-business (B2B) guests access to Microsoft Fabric, and contents that they have permissions to. With the setting turned off, B2B guest users receive an error when trying to access Power BI. The recommended state is Enabled for a subset of the organization or Disabled.", + "RationaleStatement": "Establishing and enforcing a dedicated security group prevents unauthorized access to Microsoft Fabric for guests collaborating in Azure that are new or assigned guest status from other applications. This upholds the principle of least privilege and uses role-based access control (RBAC). These security groups can also be used for tasks like conditional access, enhancing risk management and user accountability across the organization.", + "ImpactStatement": "Security groups will need to be more closely tended to and monitored.", + "RemediationProcedure": "To remediate using the UI: 1. Navigate to Microsoft Fabric https://app.powerbi.com/admin-portal. 2. Select Tenant settings. 3. Scroll to Export and Sharing settings. 4. Set Guest users can access Microsoft Fabric to one of these states: State 1: Disabled, State 2: Enabled with Specific security groups selected and defined. Important: If the organization doesn't actively use this feature it is recommended to keep it Disabled.", + "AuditProcedure": "To audit using the UI: 1. Navigate to Microsoft Fabric https://app.powerbi.com/admin-portal. 2. Select Tenant settings. 3. Scroll to Export and Sharing settings. 4. Ensure that Guest users can access Microsoft Fabric adheres to one of these states: State 1: Disabled, State 2: Enabled with Specific security groups selected and defined.", + "AdditionalInformation": "If the organization doesn't actively use this feature it is recommended to keep it Disabled.", + "References": "https://learn.microsoft.com/en-us/fabric/admin/service-admin-portal-export-sharing", + "DefaultValue": "Enabled for the entire organization" + } + ] + }, + { + "Id": "9.1.2", + "Description": "This setting helps organizations choose whether new external users can be invited to the organization through Power BI sharing, permissions, and subscription experiences. This setting only controls the ability to invite through Power BI. Ensure external user invitations are restricted.", + "Checks": [], + "Attributes": [ + { + "Section": "9 Microsoft Fabric", + "SubSection": "9.1 Tenant settings", + "Profile": "E3 Level 1", + "AssessmentStatus": "Manual", + "Description": "This setting helps organizations choose whether new external users can be invited to the organization through Power BI sharing, permissions, and subscription experiences. This setting only controls the ability to invite through Power BI. The recommended state is Enabled for a subset of the organization or Disabled.", + "RationaleStatement": "Establishing and enforcing a dedicated security group prevents unauthorized access to Microsoft Fabric for guests collaborating in Azure that are new or assigned guest status from other applications. This upholds the principle of least privilege and uses role-based access control (RBAC). These security groups can also be used for tasks like conditional access, enhancing risk management and user accountability across the organization.", + "ImpactStatement": "Guest user invitations will be limited to only specific employees.", + "RemediationProcedure": "To remediate using the UI: 1. Navigate to Microsoft Fabric https://app.powerbi.com/admin-portal. 2. Select Tenant settings. 3. Scroll to Export and Sharing settings. 4. Set Users can invite guest users to collaborate through item sharing and permissions to one of these states: State 1: Disabled, State 2: Enabled with Specific security groups selected and defined. Important: If the organization doesn't actively use this feature it is recommended to keep it Disabled.", + "AuditProcedure": "To audit using the UI: 1. Navigate to Microsoft Fabric https://app.powerbi.com/admin-portal. 2. Select Tenant settings. 3. Scroll to Export and Sharing settings. 4. Ensure that Users can invite guest users to collaborate through item sharing and permissions adheres to one of these states: State 1: Disabled, State 2: Enabled with Specific security groups selected and defined.", + "AdditionalInformation": "To invite external users to the organization, the user must also have the Microsoft Entra Guest Inviter role.", + "References": "https://learn.microsoft.com/en-us/fabric/admin/service-admin-portal-export-sharing", + "DefaultValue": "Enabled for the entire organization" + } + ] + }, + { + "Id": "9.1.3", + "Description": "This setting allows Microsoft Entra B2B guest users to have full access to the browsing experience using the left-hand navigation pane in the organization. Guest users who have been assigned workspace roles or specific item permissions will continue to have those roles and/or permissions, even if this setting is disabled. Ensure guest access to content is restricted.", + "Checks": [], + "Attributes": [ + { + "Section": "9 Microsoft Fabric", + "SubSection": "9.1 Tenant settings", + "Profile": "E3 Level 1", + "AssessmentStatus": "Manual", + "Description": "This setting allows Microsoft Entra B2B guest users to have full access to the browsing experience using the left-hand navigation pane in the organization. Guest users who have been assigned workspace roles or specific item permissions will continue to have those roles and/or permissions, even if this setting is disabled. The recommended state is Enabled for a subset of the organization or Disabled.", + "RationaleStatement": "Establishing and enforcing a dedicated security group prevents unauthorized access to Microsoft Fabric for guests collaborating in Entra that are new or assigned guest status from other applications. This upholds the principle of least privilege and uses role-based access control (RBAC). These security groups can also be used for tasks like conditional access, enhancing risk management and user accountability across the organization.", + "ImpactStatement": "Security groups will need to be more closely tended to and monitored.", + "RemediationProcedure": "To remediate using the UI: 1. Navigate to Microsoft Fabric https://app.powerbi.com/admin-portal. 2. Select Tenant settings. 3. Scroll to Export and Sharing settings. 4. Set Guest users can browse and access Fabric content to one of these states: State 1: Disabled, State 2: Enabled with Specific security groups selected and defined. Important: If the organization doesn't actively use this feature it is recommended to keep it Disabled.", + "AuditProcedure": "To audit using the UI: 1. Navigate to Microsoft Fabric https://app.powerbi.com/admin-portal. 2. Select Tenant settings. 3. Scroll to Export and Sharing settings. 4. Ensure that Guest users can browse and access Fabric content adheres to one of these states: State 1: Disabled, State 2: Enabled with Specific security groups selected and defined.", + "AdditionalInformation": "If the organization doesn't actively use this feature it is recommended to keep it Disabled.", + "References": "https://learn.microsoft.com/en-us/fabric/admin/service-admin-portal-export-sharing", + "DefaultValue": "Disabled" + } + ] + }, + { + "Id": "9.1.4", + "Description": "Power BI enables users to share reports and materials directly on the internet from both the application's desktop version and its web user interface. This functionality generates a publicly reachable web link that doesn't necessitate authentication or the need to be an Entra ID user in order to access and view it. Ensure 'Publish to web' is restricted.", + "Checks": [], + "Attributes": [ + { + "Section": "9 Microsoft Fabric", + "SubSection": "9.1 Tenant settings", + "Profile": "E3 Level 1", + "AssessmentStatus": "Manual", + "Description": "Power BI enables users to share reports and materials directly on the internet from both the application's desktop version and its web user interface. This functionality generates a publicly reachable web link that doesn't necessitate authentication or the need to be an Entra ID user in order to access and view it. The recommended state is Enabled for a subset of the organization or Disabled.", + "RationaleStatement": "When using Publish to Web anyone on the Internet can view a published report or visual. Viewing requires no authentication. It includes viewing detail-level data that your reports aggregate. By disabling the feature, restricting access to certain users and allowing existing embed codes organizations can mitigate the exposure of confidential or proprietary information.", + "ImpactStatement": "Depending on the organization's utilization administrators may experience more overhead managing embed codes, and requests.", + "RemediationProcedure": "To remediate using the UI: 1. Navigate to Microsoft Fabric https://app.powerbi.com/admin-portal. 2. Select Tenant settings. 3. Scroll to Export and Sharing settings. 4. Set Publish to web to one of these states: State 1: Disabled, State 2: Enabled with Choose how embed codes work set to Only allow existing codes AND Specific security groups selected and defined. Important: If the organization doesn't actively use this feature it is recommended to keep it Disabled.", + "AuditProcedure": "To audit using the UI: 1. Navigate to Microsoft Fabric https://app.powerbi.com/admin-portal. 2. Select Tenant settings. 3. Scroll to Export and Sharing settings. 4. Ensure that Publish to web adheres to one of these states: State 1: Disabled, State 2: Enabled with Choose how embed codes work set to Only allow existing codes AND Specific security groups selected and defined.", + "AdditionalInformation": "If the organization doesn't actively use this feature it is recommended to keep it Disabled.", + "References": "https://learn.microsoft.com/en-us/power-bi/collaborate-share/service-publish-to-web", + "DefaultValue": "Enabled for the entire organization, Only allow existing codes" + } + ] + }, + { + "Id": "9.1.5", + "Description": "Power BI allows the integration of R and Python scripts directly into visuals. This feature allows data visualizations by incorporating custom calculations, statistical analyses, machine learning models, and more using R or Python scripts. Ensure 'Interact with and share R and Python' visuals is 'Disabled'.", + "Checks": [], + "Attributes": [ + { + "Section": "9 Microsoft Fabric", + "SubSection": "9.1 Tenant settings", + "Profile": "E3 Level 2", + "AssessmentStatus": "Manual", + "Description": "Power BI allows the integration of R and Python scripts directly into visuals. This feature allows data visualizations by incorporating custom calculations, statistical analyses, machine learning models, and more using R or Python scripts. Custom visuals can be created by embedding them directly into Power BI reports. Users can then interact with these visuals and see the results of the custom code within the Power BI interface.", + "RationaleStatement": "Disabling this feature can reduce the attack surface by preventing potential malicious code execution leading to data breaches, or unauthorized access. The potential for sensitive or confidential data being leaked to unintended users is also increased with the use of scripts.", + "ImpactStatement": "Use of R and Python scripting will require exceptions for developers, along with more stringent code review.", + "RemediationProcedure": "To remediate using the UI: 1. Navigate to Microsoft Fabric https://app.powerbi.com/admin-portal. 2. Select Tenant settings. 3. Scroll to R and Python visuals settings. 4. Set Interact with and share R and Python visuals to Disabled.", + "AuditProcedure": "To audit using the UI: 1. Navigate to Microsoft Fabric https://app.powerbi.com/admin-portal. 2. Select Tenant settings. 3. Scroll to R and Python visuals settings. 4. Ensure that Interact with and share R and Python visuals is Disabled.", + "AdditionalInformation": "", + "References": "https://learn.microsoft.com/en-us/fabric/admin/service-admin-portal-r-python-visuals", + "DefaultValue": "Enabled" + } + ] + }, + { + "Id": "9.1.6", + "Description": "Information protection tenant settings help to protect sensitive information in the Power BI tenant. Allowing and applying sensitivity labels to content ensures that information is only seen and accessed by the appropriate users. Ensure 'Allow users to apply sensitivity labels for content' is 'Enabled'.", + "Checks": [], + "Attributes": [ + { + "Section": "9 Microsoft Fabric", + "SubSection": "9.1 Tenant settings", + "Profile": "E3 Level 1", + "AssessmentStatus": "Manual", + "Description": "Information protection tenant settings help to protect sensitive information in the Power BI tenant. Allowing and applying sensitivity labels to content ensures that information is only seen and accessed by the appropriate users. The recommended state is Enabled or Enabled for a subset of the organization.", + "RationaleStatement": "Establishing data classifications and affixing labels to data at creation enables organizations to discern the data's criticality, sensitivity, and value. This initial identification enables the implementation of appropriate protective measures, utilizing technologies like Data Loss Prevention (DLP) to avert inadvertent exposure and enforcing access controls to safeguard against unauthorized access. This practice can also promote user awareness and responsibility in regard to the nature of the data they interact with.", + "ImpactStatement": "Additional license requirements like Power BI Pro are required, as outlined in the Licensing and requirements page linked in the references section.", + "RemediationProcedure": "To remediate using the UI: 1. Navigate to Microsoft Fabric https://app.powerbi.com/admin-portal. 2. Select Tenant settings. 3. Scroll to Information protection. 4. Set Allow users to apply sensitivity labels for content to one of these states: State 1: Enabled, State 2: Enabled with Specific security groups selected and defined.", + "AuditProcedure": "To audit using the UI: 1. Navigate to Microsoft Fabric https://app.powerbi.com/admin-portal. 2. Select Tenant settings. 3. Scroll to Information protection. 4. Ensure that Allow users to apply sensitivity labels for content adheres to one of these states: State 1: Enabled, State 2: Enabled with Specific security groups selected and defined.", + "AdditionalInformation": "Sensitivity labels and protection are only applied to files exported to Excel, PowerPoint, or PDF files, that are controlled by 'Export to Excel' and 'Export reports as PowerPoint presentation or PDF documents' settings. All other export and sharing options do not support the application of sensitivity labels and protection.", + "References": "https://learn.microsoft.com/en-us/power-bi/enterprise/service-security-enable-data-sensitivity-labels", + "DefaultValue": "Disabled" + } + ] + }, + { + "Id": "9.1.7", + "Description": "Creating a shareable link allows a user to create a link to a report or dashboard, then add that link to an email or another messaging application. This setting solely deals with restrictions to People in the organization. Ensure shareable links are restricted.", + "Checks": [], + "Attributes": [ + { + "Section": "9 Microsoft Fabric", + "SubSection": "9.1 Tenant settings", + "Profile": "E3 Level 1", + "AssessmentStatus": "Manual", + "Description": "Creating a shareable link allows a user to create a link to a report or dashboard, then add that link to an email or another messaging application. There are 3 options that can be selected when creating a shareable link: People in your organization, People with existing access, Specific people. This setting solely deals with restrictions to People in the organization. The recommended state is Enabled for a subset of the organization or Disabled.", + "RationaleStatement": "While external users are unable to utilize shareable links, disabling or restricting this feature ensures that a user cannot generate a link accessible by individuals within the same organization who lack the necessary clearance to the shared data. This measure along with proper file and folder permissions can help prevent unintended access and potential information leakage.", + "ImpactStatement": "If the setting is Enabled then only specific people in the organization would be allowed to create general links viewable by the entire organization.", + "RemediationProcedure": "To remediate using the UI: 1. Navigate to Microsoft Fabric https://app.powerbi.com/admin-portal. 2. Select Tenant settings. 3. Scroll to Export and Sharing settings. 4. Set Allow shareable links to grant access to everyone in your organization to one of these states: State 1: Disabled, State 2: Enabled with Specific security groups selected and defined. Important: If the organization doesn't actively use this feature it is recommended to keep it Disabled.", + "AuditProcedure": "To audit using the UI: 1. Navigate to Microsoft Fabric https://app.powerbi.com/admin-portal. 2. Select Tenant settings. 3. Scroll to Export and Sharing settings. 4. Ensure that Allow shareable links to grant access to everyone in your organization adheres to one of these states: State 1: Disabled, State 2: Enabled with Specific security groups selected and defined.", + "AdditionalInformation": "External users by default are not included in any of these categories, and therefore cannot use any of these links regardless of the state of this setting.", + "References": "https://learn.microsoft.com/en-us/power-bi/collaborate-share/service-share-dashboards?wt.mc_id=powerbi_inproduct_sharedialog#link-settings", + "DefaultValue": "Enabled for the entire organization" + } + ] + }, + { + "Id": "9.1.8", + "Description": "Power BI admins can specify which users or user groups can share datasets externally with guests from a different tenant through the in-place mechanism. Disabling this setting prevents any user from sharing datasets externally by restricting the ability of users to turn on external sharing for datasets they own or manage. Ensure enabling of external data sharing is restricted.", + "Checks": [], + "Attributes": [ + { + "Section": "9 Microsoft Fabric", + "SubSection": "9.1 Tenant settings", + "Profile": "E3 Level 1", + "AssessmentStatus": "Manual", + "Description": "Power BI admins can specify which users or user groups can share datasets externally with guests from a different tenant through the in-place mechanism. Disabling this setting prevents any user from sharing datasets externally by restricting the ability of users to turn on external sharing for datasets they own or manage. The recommended state is Enabled for a subset of the organization or Disabled.", + "RationaleStatement": "Establishing and enforcing a dedicated security group prevents unauthorized access to Microsoft Fabric for guests collaborating in Azure that are new or from other applications. This upholds the principle of least privilege and uses role-based access control (RBAC). These security groups can also be used for tasks like conditional access, enhancing risk management and user accountability across the organization.", + "ImpactStatement": "Security groups will need to be more closely tended to and monitored.", + "RemediationProcedure": "To remediate using the UI: 1. Navigate to Microsoft Fabric https://app.powerbi.com/admin-portal. 2. Select Tenant settings. 3. Scroll to Export and Sharing settings. 4. Set Allow specific users to turn on external data sharing to one of these states: State 1: Disabled, State 2: Enabled with Specific security groups selected and defined. Important: If the organization doesn't actively use this feature it is recommended to keep it Disabled.", + "AuditProcedure": "To audit using the UI: 1. Navigate to Microsoft Fabric https://app.powerbi.com/admin-portal. 2. Select Tenant settings. 3. Scroll to Export and Sharing settings. 4. Ensure that Allow specific users to turn on external data sharing adheres to one of these states: State 1: Disabled, State 2: Enabled with Specific security groups selected and defined.", + "AdditionalInformation": "If the organization doesn't actively use this feature it is recommended to keep it Disabled.", + "References": "https://learn.microsoft.com/en-us/fabric/admin/service-admin-portal-export-sharing", + "DefaultValue": "Enabled for the entire organization" + } + ] + }, + { + "Id": "9.1.9", + "Description": "This setting blocks the use of resource key based authentication. The Block ResourceKey Authentication setting applies to streaming and PUSH datasets. If blocked users will not be allowed to send data to streaming and PUSH datasets using the API with a resource key. Ensure 'Block ResourceKey Authentication' is 'Enabled'.", + "Checks": [], + "Attributes": [ + { + "Section": "9 Microsoft Fabric", + "SubSection": "9.1 Tenant settings", + "Profile": "E3 Level 1", + "AssessmentStatus": "Manual", + "Description": "This setting blocks the use of resource key based authentication. The Block ResourceKey Authentication setting applies to streaming and PUSH datasets. If blocked users will not be allowed to send data to streaming and PUSH datasets using the API with a resource key. The recommended state is Enabled.", + "RationaleStatement": "Resource keys are a form of authentication that allows users to access Power BI resources (such as reports, dashboards, and datasets) without requiring individual user accounts. While convenient, this method bypasses the organization's centralized identity and access management controls. Enabling ensures that access to Power BI resources is tied to the organization's authentication mechanisms, providing a more secure and controlled environment.", + "ImpactStatement": "Developers will need to request a special exception in order to use this feature.", + "RemediationProcedure": "To remediate using the UI: 1. Navigate to Microsoft Fabric https://app.powerbi.com/admin-portal. 2. Select Tenant settings. 3. Scroll to Developer settings. 4. Set Block ResourceKey Authentication to Enabled.", + "AuditProcedure": "To audit using the UI: 1. Navigate to Microsoft Fabric https://app.powerbi.com/admin-portal. 2. Select Tenant settings. 3. Scroll to Developer settings. 4. Ensure that Block ResourceKey Authentication is Enabled.", + "AdditionalInformation": "", + "References": "https://learn.microsoft.com/en-us/fabric/admin/service-admin-portal-developer", + "DefaultValue": "Disabled for the entire organization" + } + ] + }, + { + "Id": "9.1.10", + "Description": "Use a service principal to access Fabric public APIs that include create, read, update, and delete (CRUD) operations, and are protected by a Fabric permission model. To allow an app to use service principal authentication, its service principal must be included in an allowed security group. Ensure access to APIs by service principals is restricted.", + "Checks": [], + "Attributes": [ + { + "Section": "9 Microsoft Fabric", + "SubSection": "9.1 Tenant settings", + "Profile": "E3 Level 1", + "AssessmentStatus": "Manual", + "Description": "Use a service principal to access Fabric public APIs that include create, read, update, and delete (CRUD) operations, and are protected by a Fabric permission model. To allow an app to use service principal authentication, its service principal must be included in an allowed security group. You can control who can access service principals by creating dedicated security groups and using these groups in other tenant settings. The recommended state is Enabled for a subset of the organization or Disabled.", + "RationaleStatement": "Leaving API access unrestricted increases the attack surface in the event an adversary gains access to a Service Principal. APIs are a feature-rich method for programmatic access to many areas of Power BI and should be guarded closely.", + "ImpactStatement": "Service principals will need to be members of specific security groups in order to perform public API calls.", + "RemediationProcedure": "To remediate using the UI: 1. Navigate to Microsoft Fabric https://app.powerbi.com/admin-portal. 2. Select Tenant settings. 3. Scroll to Developer settings. 4. Set Service principals can call Fabric public APIs to one of these states: State 1: Disabled, State 2: Enabled with Specific security groups selected and defined. Important: If the organization doesn't actively use this feature it is recommended to keep it Disabled.", + "AuditProcedure": "To audit using the UI: 1. Navigate to Microsoft Fabric https://app.powerbi.com/admin-portal. 2. Select Tenant settings. 3. Scroll to Developer settings. 4. Ensure that Service principals can call Fabric public APIs adheres to one of these states: State 1: Disabled, State 2: Enabled with Specific security groups selected and defined.", + "AdditionalInformation": "If the organization doesn't actively use this feature it is recommended to keep it Disabled.", + "References": "https://learn.microsoft.com/en-us/fabric/admin/service-admin-portal-developer", + "DefaultValue": "Enabled for the entire organization" + } + ] + }, + { + "Id": "9.1.11", + "Description": "Service principal profiles provide a flexible solution for apps used in a multitenancy deployment. The profiles enable customer data isolation and tighter security boundaries between customers that are utilizing the app. Ensure service principals cannot create and use profiles.", + "Checks": [], + "Attributes": [ + { + "Section": "9 Microsoft Fabric", + "SubSection": "9.1 Tenant settings", + "Profile": "E3 Level 1", + "AssessmentStatus": "Manual", + "Description": "Service principal profiles provide a flexible solution for apps used in a multitenancy deployment. The profiles enable customer data isolation and tighter security boundaries between customers that are utilizing the app. The recommended state is Enabled for a subset of the organization or Disabled.", + "RationaleStatement": "Service Principals should be restricted to a security group to limit which Service Principals can interact with profiles. This supports the principle of least privilege.", + "ImpactStatement": "Disabled is the default behavior.", + "RemediationProcedure": "To remediate using the UI: 1. Navigate to Microsoft Fabric https://app.powerbi.com/admin-portal. 2. Select Tenant settings. 3. Scroll to Developer settings. 4. Set Allow service principals to create and use profiles to one of these states: State 1: Disabled, State 2: Enabled with Specific security groups selected and defined. Important: If the organization doesn't actively use this feature it is recommended to keep it Disabled.", + "AuditProcedure": "To audit using the UI: 1. Navigate to Microsoft Fabric https://app.powerbi.com/admin-portal. 2. Select Tenant settings. 3. Scroll to Developer settings. 4. Ensure that Allow service principals to create and use profiles adheres to one of these states: State 1: Disabled, State 2: Enabled with Specific security groups selected and defined.", + "AdditionalInformation": "If the organization doesn't actively use this feature it is recommended to keep it Disabled.", + "References": "https://learn.microsoft.com/en-us/fabric/admin/service-admin-portal-developer", + "DefaultValue": "Disabled for the entire organization" + } + ] + }, + { + "Id": "9.1.12", + "Description": "Use a service principal to access these Fabric APIs that aren't protected by a Fabric permission model: Create Workspace, Create Connection, Create Deployment Pipeline. Ensure service principals ability to create workspaces, connections and deployment pipelines is restricted.", + "Checks": [], + "Attributes": [ + { + "Section": "9 Microsoft Fabric", + "SubSection": "9.1 Tenant settings", + "Profile": "E3 Level 1", + "AssessmentStatus": "Manual", + "Description": "Use a service principal to access these Fabric APIs that aren't protected by a Fabric permission model: Create Workspace, Create Connection, Create Deployment Pipeline. To allow an app to use service principal authentication, its service principal must be included in an allowed security group. You can control who can access service principals by creating dedicated security groups and using these groups in other tenant settings. The recommended state is Enabled for a subset of the organization or Disabled.", + "RationaleStatement": "Leaving API access unrestricted increases the attack surface in the event an adversary gains access to a Service Principal. APIs are a feature-rich method for programmatic access to many areas of Power BI and should be guarded closely.", + "ImpactStatement": "Service principals will need to be members of specific security groups in order to perform public API calls.", + "RemediationProcedure": "To remediate using the UI: 1. Navigate to Microsoft Fabric https://app.powerbi.com/admin-portal. 2. Select Tenant settings. 3. Scroll to Developer settings. 4. Set Service principals can create workspaces, connections, and deployment pipelines to one of these states: State 1: Disabled, State 2: Enabled with Specific security groups selected and defined. Important: If the organization doesn't actively use this feature it is recommended to keep it Disabled.", + "AuditProcedure": "To audit using the UI: 1. Navigate to Microsoft Fabric https://app.powerbi.com/admin-portal. 2. Select Tenant settings. 3. Scroll to Developer settings. 4. Ensure that Service principals can create workspaces, connections, and deployment pipelines adheres to one of these states: State 1: Disabled, State 2: Enabled with Specific security groups selected and defined.", + "AdditionalInformation": "If the organization doesn't actively use this feature it is recommended to keep it Disabled.", + "References": "https://learn.microsoft.com/en-us/fabric/admin/service-admin-portal-developer", + "DefaultValue": "Disabled for the entire organization" + } + ] + } + ] +} diff --git a/prowler/providers/aws/aws_regions_by_service.json b/prowler/providers/aws/aws_regions_by_service.json index 5dd41193ca..91ea6c6be3 100644 --- a/prowler/providers/aws/aws_regions_by_service.json +++ b/prowler/providers/aws/aws_regions_by_service.json @@ -1023,6 +1023,7 @@ "ap-southeast-3", "ap-southeast-4", "ap-southeast-5", + "ap-southeast-6", "ap-southeast-7", "ca-central-1", "ca-west-1", @@ -3714,6 +3715,7 @@ "ap-south-2", "ap-southeast-1", "ap-southeast-2", + "ap-southeast-3", "ap-southeast-5", "ap-southeast-7", "ca-central-1", @@ -4418,23 +4420,6 @@ ] } }, - "elastictranscoder": { - "regions": { - "aws": [ - "ap-northeast-1", - "ap-south-1", - "ap-southeast-1", - "ap-southeast-2", - "eu-west-1", - "us-east-1", - "us-west-1", - "us-west-2" - ], - "aws-cn": [], - "aws-eusc": [], - "aws-us-gov": [] - } - }, "elb": { "regions": { "aws": [ @@ -4847,24 +4832,6 @@ ] } }, - "evidently": { - "regions": { - "aws": [ - "ap-northeast-1", - "ap-southeast-1", - "ap-southeast-2", - "eu-central-1", - "eu-north-1", - "eu-west-1", - "us-east-1", - "us-east-2", - "us-west-2" - ], - "aws-cn": [], - "aws-eusc": [], - "aws-us-gov": [] - } - }, "evs": { "regions": { "aws": [ @@ -5582,6 +5549,7 @@ "ap-southeast-3", "ap-southeast-4", "ap-southeast-5", + "ap-southeast-6", "ap-southeast-7", "ca-central-1", "ca-west-1", @@ -6563,6 +6531,7 @@ "ap-southeast-3", "ap-southeast-4", "ap-southeast-5", + "ap-southeast-6", "ap-southeast-7", "ca-central-1", "ca-west-1", @@ -6613,6 +6582,7 @@ "ap-southeast-3", "ap-southeast-4", "ap-southeast-5", + "ap-southeast-6", "ap-southeast-7", "ca-central-1", "ca-west-1", @@ -6639,7 +6609,10 @@ "cn-northwest-1" ], "aws-eusc": [], - "aws-us-gov": [] + "aws-us-gov": [ + "us-gov-east-1", + "us-gov-west-1" + ] } }, "kendra": { @@ -6883,6 +6856,7 @@ "ap-southeast-3", "ap-southeast-4", "ap-southeast-5", + "ap-southeast-6", "ap-southeast-7", "ca-central-1", "ca-west-1", @@ -7340,24 +7314,6 @@ "aws-us-gov": [] } }, - "lookoutmetrics": { - "regions": { - "aws": [ - "ap-northeast-1", - "ap-southeast-1", - "ap-southeast-2", - "eu-central-1", - "eu-north-1", - "eu-west-1", - "us-east-1", - "us-east-2", - "us-west-2" - ], - "aws-cn": [], - "aws-eusc": [], - "aws-us-gov": [] - } - }, "lumberyard": { "regions": { "aws": [ @@ -8618,7 +8574,10 @@ "odb": { "regions": { "aws": [ + "ap-northeast-1", + "eu-central-1", "us-east-1", + "us-east-2", "us-west-2" ], "aws-cn": [], @@ -9422,26 +9381,6 @@ "aws-us-gov": [] } }, - "qldb": { - "regions": { - "aws": [ - "ap-northeast-1", - "ap-northeast-2", - "ap-southeast-1", - "ap-southeast-2", - "ca-central-1", - "eu-central-1", - "eu-west-1", - "eu-west-2", - "us-east-1", - "us-east-2", - "us-west-2" - ], - "aws-cn": [], - "aws-eusc": [], - "aws-us-gov": [] - } - }, "quicksight": { "regions": { "aws": [ @@ -9781,6 +9720,7 @@ "ap-southeast-2", "ap-southeast-3", "ap-southeast-5", + "ap-southeast-6", "ap-southeast-7", "ca-central-1", "eu-central-1", @@ -12533,6 +12473,7 @@ "ap-southeast-3", "ap-southeast-4", "ap-southeast-5", + "ap-southeast-6", "ap-southeast-7", "ca-central-1", "ca-west-1", @@ -12980,6 +12921,7 @@ "ap-southeast-3", "ap-southeast-4", "ap-southeast-5", + "ap-southeast-6", "ap-southeast-7", "ca-central-1", "ca-west-1", @@ -13073,6 +13015,7 @@ "ap-southeast-3", "ap-southeast-4", "ap-southeast-5", + "ap-southeast-6", "ap-southeast-7", "ca-central-1", "ca-west-1", @@ -13360,4 +13303,4 @@ } } } -} +} \ No newline at end of file diff --git a/prowler/providers/aws/services/bedrock/bedrock_agent_guardrail_enabled/bedrock_agent_guardrail_enabled.metadata.json b/prowler/providers/aws/services/bedrock/bedrock_agent_guardrail_enabled/bedrock_agent_guardrail_enabled.metadata.json index 89c7b1fd76..c61ec9e5dc 100644 --- a/prowler/providers/aws/services/bedrock/bedrock_agent_guardrail_enabled/bedrock_agent_guardrail_enabled.metadata.json +++ b/prowler/providers/aws/services/bedrock/bedrock_agent_guardrail_enabled/bedrock_agent_guardrail_enabled.metadata.json @@ -1,27 +1,36 @@ { "Provider": "aws", "CheckID": "bedrock_agent_guardrail_enabled", - "CheckTitle": "Ensure that Guardrails are enabled for Amazon Bedrock agent sessions.", - "CheckType": [], + "CheckTitle": "Amazon Bedrock agent uses a guardrail to protect agent sessions", + "CheckType": [ + "Software and Configuration Checks/AWS Security Best Practices/Runtime Behavior Analysis", + "Effects/Data Exposure" + ], "ServiceName": "bedrock", "SubServiceName": "", - "ResourceIdTemplate": "arn:partition:bedrock:region:account-id:agent/resource-id", + "ResourceIdTemplate": "", "Severity": "high", "ResourceType": "Other", "ResourceGroup": "ai_ml", - "Description": "This check ensures that Guardrails are enabled to protect Amazon Bedrock agent sessions. Guardrails help mitigate security risks by filtering and blocking harmful or sensitive content during interactions with AI models.", - "Risk": "Without guardrails enabled, Amazon Bedrock agent sessions are vulnerable to harmful prompts or inputs that could expose sensitive information or generate inappropriate content. This could lead to privacy violations, data leaks, or other security risks.", - "RelatedUrl": "https://docs.aws.amazon.com/bedrock/latest/userguide/guardrails.html", + "Description": "**Bedrock agents** should have an associated **guardrail** for their sessions. The evaluation identifies agents without a guardrail linked for input/output screening during interactions.", + "Risk": "Without **guardrails**, agent exchanges may expose **PII** or internal data (confidentiality), accept **prompt injections** that manipulate tool calls or outputs (integrity), and produce unsafe or out-of-scope responses that erode trust and cause policy violations.", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://docs.aws.amazon.com/bedrock/latest/userguide/guardrails-create.html", + "https://docs.aws.amazon.com/bedrock/latest/userguide/agents-guardrail.html", + "https://www.trendmicro.com/cloudoneconformity/knowledge-base/aws/Bedrock/protect-agent-sessions-with-guardrails.html", + "https://docs.aws.amazon.com/bedrock/latest/userguide/guardrails.html" + ], "Remediation": { "Code": { - "CLI": "", - "NativeIaC": "", - "Other": "https://www.trendmicro.com/cloudoneconformity/knowledge-base/aws/Bedrock/protect-agent-sessions-with-guardrails.html", - "Terraform": "" + "CLI": "aws bedrock-agent update-agent --agent-id --agent-name --agent-resource-role-arn --foundation-model --guardrail-configuration guardrailIdentifier=,guardrailVersion=DRAFT", + "NativeIaC": "```yaml\n# CloudFormation: Associate a guardrail with a Bedrock Agent\nResources:\n ExampleAgent:\n Type: AWS::Bedrock::Agent\n Properties:\n AgentName: \n AgentResourceRoleArn: \n FoundationModel: \n Instruction: \"\"\n GuardrailConfiguration: # CRITICAL: associates a guardrail to protect sessions\n GuardrailIdentifier: # CRITICAL: guardrail ID used by the agent\n GuardrailVersion: DRAFT # CRITICAL: version applied\n```", + "Other": "1. Open the Amazon Bedrock console and go to Agents\n2. Select the agent and click Edit\n3. In Guardrail details, select an existing guardrail and its version (e.g., DRAFT)\n4. Click Save (deploy changes if prompted)\n5. Verify the agent now shows the selected guardrail", + "Terraform": "```hcl\n# Terraform (AWS Cloud Control): Associate a guardrail with a Bedrock Agent\nresource \"awscc_bedrock_agent\" \"example\" {\n agent_name = \"\"\n agent_resource_role_arn = \"\"\n foundation_model = \"\"\n instruction = \"\"\n\n # CRITICAL: associates a guardrail to protect agent sessions\n guardrail_configuration {\n guardrail_identifier = \"\" # CRITICAL: guardrail ID\n guardrail_version = \"DRAFT\" # CRITICAL: version applied\n }\n}\n```" }, "Recommendation": { - "Text": "Enable Guardrails for Amazon Bedrock agent sessions to protect against harmful inputs and outputs during interactions.", - "Url": "https://docs.aws.amazon.com/bedrock/latest/userguide/guardrails-create.html" + "Text": "Associate a **guardrail** with every agent and tailor policies to your use case:\n- Enable content/word filters, denied topics, and sensitive-data masking\n- Use contextual grounding for RAG where relevant\n- Test and iterate across versions\nApply **least privilege** to agent tools and use **defense in depth** with monitoring and review.", + "Url": "https://hub.prowler.com/check/bedrock_agent_guardrail_enabled" } }, "Categories": [ diff --git a/prowler/providers/aws/services/bedrock/bedrock_api_key_no_administrative_privileges/bedrock_api_key_no_administrative_privileges.metadata.json b/prowler/providers/aws/services/bedrock/bedrock_api_key_no_administrative_privileges/bedrock_api_key_no_administrative_privileges.metadata.json index 160654a5aa..9b306024b6 100644 --- a/prowler/providers/aws/services/bedrock/bedrock_api_key_no_administrative_privileges/bedrock_api_key_no_administrative_privileges.metadata.json +++ b/prowler/providers/aws/services/bedrock/bedrock_api_key_no_administrative_privileges/bedrock_api_key_no_administrative_privileges.metadata.json @@ -1,35 +1,42 @@ { "Provider": "aws", "CheckID": "bedrock_api_key_no_administrative_privileges", - "CheckTitle": "Ensure Amazon Bedrock API keys do not have administrative privileges or privilege escalation", + "CheckTitle": "Amazon Bedrock API key does not have administrative privileges, privilege escalation paths, or full Bedrock service access", "CheckType": [ - "Software and Configuration Checks", - "Industry and Regulatory Standards" + "Software and Configuration Checks/AWS Security Best Practices", + "Software and Configuration Checks/Industry and Regulatory Standards/AWS Foundational Security Best Practices", + "Software and Configuration Checks/Industry and Regulatory Standards/CIS AWS Foundations Benchmark", + "TTPs/Privilege Escalation" ], "ServiceName": "bedrock", "SubServiceName": "", - "ResourceIdTemplate": "arn:partition:iam:region:account-id:user/{user-name}/credential/{api-key-id}", + "ResourceIdTemplate": "", "Severity": "high", - "ResourceType": "AwsIamServiceSpecificCredential", + "ResourceType": "AwsIamAccessKey", "ResourceGroup": "IAM", - "Description": "Ensure that Amazon Bedrock API keys do not have administrative privileges or privilege escalation capabilities. API keys with administrative privileges can perform any action on any resource in your AWS environment, while privilege escalation allows users to grant themselves additional permissions, both posing significant security risks.", - "Risk": "Amazon Bedrock API keys with administrative privileges can perform any action on any resource in your AWS environment. Privilege escalation capabilities allow users to grant themselves additional permissions beyond their intended scope. Both violations of the principle of least privilege can lead to security vulnerabilities, data leaks, data loss, or unexpected charges if the API key is compromised or misused.", - "RelatedUrl": "https://docs.aws.amazon.com/bedrock/latest/userguide/api-keys.html", + "Description": "**Bedrock API keys** linked to IAM users are evaluated for excessive permissions, including policies that grant full access (`*` or `bedrock:*`) or enable **privilege escalation**. The finding highlights keys whose attached or inline policies provide broad or escalating capabilities.", + "Risk": "Over-privileged Bedrock API keys weaken confidentiality, integrity, and availability. If compromised, an attacker could:\n- Escalate IAM rights and persist access\n- Invoke models at scale to exfiltrate data or incur high costs\n- Modify Bedrock settings, disrupting operations", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://docs.aws.amazon.com/IAM/latest/UserGuide/best-practices.html#grant-least-privilege", + "https://docs.aws.amazon.com/IAM/latest/UserGuide/getting-started-reduce-permissions.html", + "https://docs.aws.amazon.com/bedrock/latest/userguide/api-keys.html" + ], "Remediation": { "Code": { "CLI": "aws iam delete-service-specific-credential --user-name --service-specific-credential-id ", - "NativeIaC": "", - "Other": "", - "Terraform": "" + "NativeIaC": "```yaml\n# CloudFormation: attach least-privilege policy to the IAM user owning the Bedrock API key\nResources:\n :\n Type: AWS::IAM::Policy\n Properties:\n PolicyName: least-priv-bedrock\n Users:\n - \n PolicyDocument:\n Version: '2012-10-17'\n Statement:\n - Effect: Allow\n Action:\n - bedrock:InvokeModel # CRITICAL: allow only needed Bedrock action to avoid admin or bedrock:* permissions\n Resource: \"*\" # Limits access scope to only InvokeModel on any resource\n```", + "Other": "1. Open the AWS Console and go to IAM > Users\n2. Select the user that owns the Bedrock service-specific credential (Security credentials > Service-specific credentials shows bedrock.amazonaws.com)\n3. In the Permissions tab, detach any policy granting AdministratorAccess or bedrock:* (e.g., AmazonBedrockFullAccess)\n4. In the same tab, delete any inline policy that provides admin/privilege-escalation permissions or bedrock:* access\n5. If Bedrock access is needed, add a minimal policy allowing only bedrock:InvokeModel\n6. Save changes", + "Terraform": "```hcl\n# Attach a minimal inline policy to the IAM user owning the Bedrock API key\nresource \"aws_iam_user_policy\" \"\" {\n name = \"least-priv-bedrock\"\n user = \"\"\n\n # CRITICAL: allow only the specific action required; avoids admin or bedrock:* full access\n policy = jsonencode({\n Version = \"2012-10-17\"\n Statement = [{\n Effect = \"Allow\"\n Action = [\"bedrock:InvokeModel\"]\n Resource = \"*\"\n }]\n })\n}\n```" }, "Recommendation": { - "Text": "Apply the principle of least privilege to Amazon Bedrock API keys. Instead of granting administrative privileges or privilege escalation capabilities, assign only the permissions necessary for specific tasks. Create custom IAM policies with minimal permissions based on the principle of least privilege. Regularly review and audit API key permissions to ensure they cannot be used for privilege escalation.", - "Url": "https://docs.aws.amazon.com/IAM/latest/UserGuide/best-practices.html#grant-least-privilege" + "Text": "Enforce **least privilege** on Bedrock keys:\n- Avoid wildcards like `*` and `bedrock:*`; allow only required actions\n- Prevent identity changes by disallowing `iam:*`\n- Prefer short-term credentials with rotation and MFA\n- Use permissions boundaries and SCPs as guardrails\n- Review usage and tighten policies via access analysis", + "Url": "https://hub.prowler.com/check/bedrock_api_key_no_administrative_privileges" } }, "Categories": [ "gen-ai", - "trust-boundaries" + "identity-access" ], "DependsOn": [], "RelatedTo": [], diff --git a/prowler/providers/aws/services/bedrock/bedrock_api_key_no_long_term_credentials/bedrock_api_key_no_long_term_credentials.metadata.json b/prowler/providers/aws/services/bedrock/bedrock_api_key_no_long_term_credentials/bedrock_api_key_no_long_term_credentials.metadata.json index 60d7d77ea9..f9b8ee0df9 100644 --- a/prowler/providers/aws/services/bedrock/bedrock_api_key_no_long_term_credentials/bedrock_api_key_no_long_term_credentials.metadata.json +++ b/prowler/providers/aws/services/bedrock/bedrock_api_key_no_long_term_credentials/bedrock_api_key_no_long_term_credentials.metadata.json @@ -1,35 +1,42 @@ { "Provider": "aws", "CheckID": "bedrock_api_key_no_long_term_credentials", - "CheckTitle": "Ensure Amazon Bedrock API keys are not long-term credentials", + "CheckTitle": "Amazon Bedrock API key is expired", "CheckType": [ - "Software and Configuration Checks", - "Industry and Regulatory Standards" + "Software and Configuration Checks/AWS Security Best Practices", + "Software and Configuration Checks/Industry and Regulatory Standards/AWS Foundational Security Best Practices", + "Software and Configuration Checks/Industry and Regulatory Standards/CIS AWS Foundations Benchmark", + "TTPs/Initial Access/Valid Accounts" ], "ServiceName": "bedrock", "SubServiceName": "", - "ResourceIdTemplate": "arn:partition:iam:region:account-id:user/{user-name}/credential/{api-key-id}", + "ResourceIdTemplate": "", "Severity": "high", - "ResourceType": "AwsIamServiceSpecificCredential", + "ResourceType": "AwsIamUser", "ResourceGroup": "IAM", - "Description": "Ensure that Amazon Bedrock API keys have expiration dates set to prevent long-term credential exposure. Long-term credentials pose a significant security risk as they remain valid indefinitely and can be used for unauthorized access if compromised.", - "Risk": "Amazon Bedrock API keys without expiration dates are long-term credentials that remain valid indefinitely. This increases the risk of unauthorized access if the credentials are compromised, as they cannot be automatically invalidated. Long-term credentials violate the principle of credential rotation and can lead to security vulnerabilities, data breaches, or unauthorized usage of Bedrock services.", - "RelatedUrl": "https://docs.aws.amazon.com/bedrock/latest/userguide/api-keys.html", + "Description": "**Bedrock API keys** are evaluated for **lifetime** and **expiration**.\n\nThe finding identifies keys that are long-lived, set to expire far in the future, or configured to `never expire`, and distinguishes them from keys that have already expired.", + "Risk": "Long-lived or non-expiring keys enable persistent access if compromised.\n- Confidentiality: unauthorized inference and exposure of prompts/outputs\n- Availability/Cost: uncontrolled usage and spend spikes\n- Integrity: actions can continue without timely revocation or rotation", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://docs.aws.amazon.com/ja_jp/bedrock/latest/userguide/getting-started-api-keys.html", + "https://docs.aws.amazon.com/IAM/latest/UserGuide/best-practices.html#rotate-credentials", + "https://docs.aws.amazon.com/bedrock/latest/userguide/api-keys.html" + ], "Remediation": { "Code": { "CLI": "aws iam delete-service-specific-credential --user-name --service-specific-credential-id ", "NativeIaC": "", - "Other": "", + "Other": "1. Sign in to the AWS Management Console and open IAM\n2. Go to Users > select > Security credentials\n3. In \"API keys for Amazon Bedrock\", find the non-expired key and click Delete\n4. Confirm deletion to remove the key (removes the long-term credential so the check passes)", "Terraform": "" }, "Recommendation": { - "Text": "Delete the long-term API keys for Amazon Bedrock. Instead, use temporary credentials, IAM roles, or create new API keys with appropriate expiration dates. Implement a credential rotation policy to ensure all API keys have reasonable expiration periods. Consider using AWS STS for temporary credentials when possible.", - "Url": "https://docs.aws.amazon.com/IAM/latest/UserGuide/best-practices.html#rotate-credentials" + "Text": "Prefer **short-term credentials** and **IAM roles**; avoid `never expire`.\n\nEnforce **least privilege**, strict **rotation**, and automatic **expiration** for any long-term key. Store secrets securely, monitor with audit logs, and revoke unused or stale keys quickly.", + "Url": "https://hub.prowler.com/check/bedrock_api_key_no_long_term_credentials" } }, "Categories": [ - "gen-ai", - "trust-boundaries" + "secrets", + "gen-ai" ], "DependsOn": [], "RelatedTo": [], diff --git a/prowler/providers/aws/services/bedrock/bedrock_guardrail_prompt_attack_filter_enabled/bedrock_guardrail_prompt_attack_filter_enabled.metadata.json b/prowler/providers/aws/services/bedrock/bedrock_guardrail_prompt_attack_filter_enabled/bedrock_guardrail_prompt_attack_filter_enabled.metadata.json index fa8dbb81f2..e2017df2f8 100644 --- a/prowler/providers/aws/services/bedrock/bedrock_guardrail_prompt_attack_filter_enabled/bedrock_guardrail_prompt_attack_filter_enabled.metadata.json +++ b/prowler/providers/aws/services/bedrock/bedrock_guardrail_prompt_attack_filter_enabled/bedrock_guardrail_prompt_attack_filter_enabled.metadata.json @@ -1,27 +1,37 @@ { "Provider": "aws", "CheckID": "bedrock_guardrail_prompt_attack_filter_enabled", - "CheckTitle": "Configure Prompt Attack Filter with the highest strength for Amazon Bedrock Guardrails.", - "CheckType": [], + "CheckTitle": "Amazon Bedrock guardrail has prompt attack filter strength set to HIGH", + "CheckType": [ + "Software and Configuration Checks/AWS Security Best Practices/Runtime Behavior Analysis", + "TTPs/Defense Evasion", + "Effects/Data Exposure" + ], "ServiceName": "bedrock", "SubServiceName": "", - "ResourceIdTemplate": "arn:partition:bedrock:region:account-id:guardrails/resource-id", + "ResourceIdTemplate": "", "Severity": "high", "ResourceType": "Other", "ResourceGroup": "ai_ml", - "Description": "Ensure that prompt attack filter strength is set to HIGH for Amazon Bedrock guardrails to mitigate prompt injection and bypass techniques.", - "Risk": "If prompt attack filter strength is not set to HIGH, Bedrock models may be more vulnerable to prompt injection attacks or jailbreak attempts, which could allow harmful or sensitive content to bypass filters and reach end users.", - "RelatedUrl": "https://docs.aws.amazon.com/bedrock/latest/userguide/guardrails.html", + "Description": "**Bedrock guardrails** have the **Prompt attack** filter set to `HIGH` strength to detect and block injection and jailbreak patterns. Guardrails missing this setting or using lower strengths are identified.", + "Risk": "Without **HIGH** prompt-attack filtering, models are exposed to **prompt injection/jailbreaks**:\n- Confidentiality: coerced disclosure of sensitive data\n- Integrity: policy evasion and manipulated outputs\n- Operations: unintended tool execution and workflow tampering", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://trendmicro.com/cloudoneconformity/knowledge-base/aws/Bedrock/prompt-attack-strength.html", + "https://docs.aws.amazon.com/bedrock/latest/userguide/prompt-injection.html", + "https://support.icompaas.com/support/solutions/articles/62000233535-ensure-prompt-attack-filter-is-configured-at-highest-strength-for-amazon-bedrock-guardrails", + "https://docs.aws.amazon.com/bedrock/latest/userguide/guardrails.html" + ], "Remediation": { "Code": { - "CLI": "aws bedrock put-guardrails-configuration --guardrails-config 'promptAttackStrength=HIGH'", - "NativeIaC": "", - "Other": "https://www.trendmicro.com/cloudoneconformity/knowledge-base/aws/Bedrock/prompt-attack-strength.html", - "Terraform": "" + "CLI": "aws bedrock update-guardrail --guardrail-identifier --content-policy-config 'filtersConfig=[{type=PROMPT_ATTACK,inputStrength=HIGH}]'", + "NativeIaC": "```yaml\nResources:\n :\n Type: AWS::Bedrock::Guardrail\n Properties:\n Name: \n BlockedInputMessaging: \"Blocked\"\n BlockedOutputsMessaging: \"Blocked\"\n ContentPolicyConfig:\n FiltersConfig:\n - Type: PROMPT_ATTACK # Critical: enables the Prompt Attack filter\n InputStrength: HIGH # Critical: sets filter strength to HIGH to pass the check\n```", + "Other": "1. Open the AWS Console and go to Amazon Bedrock\n2. Select Guardrails, then choose your guardrail\n3. In Content filters, find Prompt attacks\n4. Set Strength to High\n5. Click Save", + "Terraform": "```hcl\nresource \"aws_bedrock_guardrail\" \"\" {\n name = \"\"\n blocked_input_messaging = \"Blocked\"\n blocked_outputs_messaging = \"Blocked\"\n\n content_policy_config {\n filters_config {\n type = \"PROMPT_ATTACK\" # Critical: enables the Prompt Attack filter\n input_strength = \"HIGH\" # Critical: sets filter strength to HIGH to pass the check\n }\n }\n}\n```" }, "Recommendation": { - "Text": "Set the prompt attack filter strength to HIGH for Amazon Bedrock guardrails to prevent prompt injection attacks and ensure robust protection against content manipulation.", - "Url": "https://docs.aws.amazon.com/bedrock/latest/userguide/prompt-injection.html" + "Text": "Set the **Prompt attack** filter to `HIGH` and apply **defense in depth**:\n- Tag user/external inputs as untrusted for evaluation\n- Combine with denied topics and sensitive-info filters\n- Enforce **least privilege** and approvals for risky actions\n- Monitor guardrail hits and tune to reduce false negatives", + "Url": "https://hub.prowler.com/check/bedrock_guardrail_prompt_attack_filter_enabled" } }, "Categories": [ diff --git a/prowler/providers/aws/services/bedrock/bedrock_guardrail_sensitive_information_filter_enabled/bedrock_guardrail_sensitive_information_filter_enabled.metadata.json b/prowler/providers/aws/services/bedrock/bedrock_guardrail_sensitive_information_filter_enabled/bedrock_guardrail_sensitive_information_filter_enabled.metadata.json index daacdc8bd9..83876b04a6 100644 --- a/prowler/providers/aws/services/bedrock/bedrock_guardrail_sensitive_information_filter_enabled/bedrock_guardrail_sensitive_information_filter_enabled.metadata.json +++ b/prowler/providers/aws/services/bedrock/bedrock_guardrail_sensitive_information_filter_enabled/bedrock_guardrail_sensitive_information_filter_enabled.metadata.json @@ -1,27 +1,37 @@ { "Provider": "aws", "CheckID": "bedrock_guardrail_sensitive_information_filter_enabled", - "CheckTitle": "Configure Sensitive Information Filters for Amazon Bedrock Guardrails.", - "CheckType": [], + "CheckTitle": "Amazon Bedrock guardrail blocks or masks sensitive information", + "CheckType": [ + "Software and Configuration Checks/AWS Security Best Practices", + "Software and Configuration Checks/AWS Security Best Practices/Runtime Behavior Analysis", + "Effects/Data Exposure", + "Sensitive Data Identifications/PII" + ], "ServiceName": "bedrock", "SubServiceName": "", - "ResourceIdTemplate": "arn:partition:bedrock:region:account-id:guardrails/resource-id", + "ResourceIdTemplate": "", "Severity": "high", "ResourceType": "Other", "ResourceGroup": "ai_ml", - "Description": "Ensure that sensitive information filters are enabled for Amazon Bedrock guardrails to prevent the leakage of sensitive data such as personally identifiable information (PII), financial data, or confidential corporate information.", - "Risk": "If sensitive information filters are not enabled, Bedrock models may inadvertently generate or expose confidential or sensitive information in responses, leading to data breaches, regulatory violations, or reputational damage.", - "RelatedUrl": "https://docs.aws.amazon.com/bedrock/latest/userguide/guardrails.html", + "Description": "**Bedrock guardrails** use **sensitive information filters** to `block` or `mask` detected PII and custom pattern matches in prompts and responses.\n\nThe evaluation looks for guardrails with this filtering configured.", + "Risk": "Absent filtering, prompts or outputs can reveal **PII**, credentials, or financial records, compromising **confidentiality**.\n- Exposed tokens enable unauthorized access and data tampering (integrity)\n- Disclosed customer details facilitate fraud and identity theft, with potential lateral movement", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://docs.aws.amazon.com/bedrock/latest/userguide/guardrails-sensitive-filters.html", + "https://www.trendmicro.com/cloudoneconformity/knowledge-base/aws/Bedrock/guardrails-with-pii-mask-block.html", + "https://docs.aws.amazon.com/bedrock/latest/userguide/guardrails.html" + ], "Remediation": { "Code": { - "CLI": "aws bedrock put-guardrails-configuration --guardrails-config 'sensitiveInformationFilter=true'", + "CLI": "aws bedrock update-guardrail --guardrail-identifier --sensitive-information-policy-config '{\"piiEntitiesConfig\":[{\"type\":\"EMAIL\",\"action\":\"ANONYMIZE\"}]}'", "NativeIaC": "", - "Other": "https://www.trendmicro.com/cloudoneconformity/knowledge-base/aws/Bedrock/guardrails-with-pii-mask-block.html", + "Other": "1. Sign in to the AWS Console and open Amazon Bedrock\n2. Go to Guardrails and select \n3. Click Edit (or Open draft) and open Sensitive information filters\n4. Add PII type EMAIL and set action to Mask (or Block)\n5. Click Save", "Terraform": "" }, "Recommendation": { - "Text": "Enable sensitive information filters for Amazon Bedrock guardrails to prevent the exposure of sensitive or confidential information.", - "Url": "https://docs.aws.amazon.com/bedrock/latest/userguide/guardrails-sensitive-filters.html" + "Text": "Enable and tune **sensitive information filters** for inputs and outputs.\n- Use `BLOCK` for high-risk disclosures; `ANONYMIZE` when context is needed\n- Add custom regex for org-specific IDs\n- Apply least privilege and data minimization\n- Test regularly and monitor outcomes as part of defense-in-depth", + "Url": "https://hub.prowler.com/check/bedrock_guardrail_sensitive_information_filter_enabled" } }, "Categories": [ diff --git a/prowler/providers/aws/services/bedrock/bedrock_model_invocation_logging_enabled/bedrock_model_invocation_logging_enabled.metadata.json b/prowler/providers/aws/services/bedrock/bedrock_model_invocation_logging_enabled/bedrock_model_invocation_logging_enabled.metadata.json index e301803e02..104df26fcf 100644 --- a/prowler/providers/aws/services/bedrock/bedrock_model_invocation_logging_enabled/bedrock_model_invocation_logging_enabled.metadata.json +++ b/prowler/providers/aws/services/bedrock/bedrock_model_invocation_logging_enabled/bedrock_model_invocation_logging_enabled.metadata.json @@ -1,27 +1,35 @@ { "Provider": "aws", "CheckID": "bedrock_model_invocation_logging_enabled", - "CheckTitle": "Ensure that model invocation logging is enabled for Amazon Bedrock.", - "CheckType": [], + "CheckTitle": "Amazon Bedrock model invocation logging is enabled", + "CheckType": [ + "Software and Configuration Checks/AWS Security Best Practices", + "Software and Configuration Checks/Industry and Regulatory Standards/AWS Foundational Security Best Practices" + ], "ServiceName": "bedrock", "SubServiceName": "", - "ResourceIdTemplate": "arn:partition:bedrock:region:account-id:model/resource-id", + "ResourceIdTemplate": "", "Severity": "medium", "ResourceType": "Other", "ResourceGroup": "ai_ml", - "Description": "Ensure that model invocation logging is enabled for Amazon Bedrock service in order to collect metadata, requests, and responses for all model invocations in your AWS cloud account.", - "Risk": "In Amazon Bedrock, model invocation logging enables you to collect the invocation request and response data, along with metadata, for all 'Converse', 'ConverseStream', 'InvokeModel', and 'InvokeModelWithResponseStream' API calls in your AWS account. Each log entry includes important details such as the timestamp, request ID, model ID, and token usage. Invocation logs can be utilized for troubleshooting, performance enhancements, abuse detection, and security auditing. By default, model invocation logging is disabled.", - "RelatedUrl": "https://docs.aws.amazon.com/bedrock/latest/userguide/model-invocation-logging.html", + "Description": "**Bedrock** model invocation logging captures request, response, and metadata for `Converse`, `ConverseStream`, `InvokeModel`, and `InvokeModelWithResponseStream` calls per Region, delivering records to **CloudWatch Logs** and/or **S3** when configured.", + "Risk": "Without **invocation logs**, you lose **auditability** and **forensic visibility** into model activity.\n\nCredential misuse or **prompt injection/jailbreak** attempts may go unnoticed, enabling data exfiltration and unauthorized spend. Missing traceability weakens **integrity** controls and slows incident response.", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://docs.aws.amazon.com/bedrock/latest/userguide/model-invocation-logging.html#model-invocation-logging-console", + "https://www.trendmicro.com/cloudoneconformity/knowledge-base/aws/Bedrock/enable-model-invocation-logging.html", + "https://docs.aws.amazon.com/bedrock/latest/userguide/model-invocation-logging.html" + ], "Remediation": { "Code": { - "CLI": "aws bedrock put-model-invocation-logging-configuration --logging-config 's3Config={bucketName='tm-bedrock-logging-data',keyPrefix='invocation-logs'},textDataDeliveryEnabled=true,imageDataDeliveryEnabled=true,embeddingDataDeliveryEnabled=true'", + "CLI": "aws bedrock put-model-invocation-logging-configuration --logging-config '{\"s3Config\":{\"bucketName\":\"\"},\"textDataDeliveryEnabled\":true}'", "NativeIaC": "", - "Other": "https://www.trendmicro.com/cloudoneconformity/knowledge-base/aws/Bedrock/enable-model-invocation-logging.html", + "Other": "1. Open the Amazon Bedrock console in the target Region\n2. Go to Settings > Model invocation logging\n3. Toggle Logging to On\n4. Select Amazon S3 as the destination and choose bucket\n5. Under Data types, select Text\n6. Click Save", "Terraform": "" }, "Recommendation": { - "Text": "Enable model invocation logging for Amazon Bedrock service in order to collect metadata, requests, and responses for all model invocations in your AWS cloud account.", - "Url": "https://docs.aws.amazon.com/bedrock/latest/userguide/model-invocation-logging.html#model-invocation-logging-console" + "Text": "Enable **model invocation logging** and route events to **CloudWatch Logs** and/or **S3**.\n\nEnforce **least privilege** on log access, use encryption, and set retention/lifecycle policies. Monitor for anomalies and alerts to support **defense in depth** and **separation of duties**.", + "Url": "https://hub.prowler.com/check/bedrock_model_invocation_logging_enabled" } }, "Categories": [ diff --git a/prowler/providers/aws/services/bedrock/bedrock_model_invocation_logs_encryption_enabled/bedrock_model_invocation_logs_encryption_enabled.metadata.json b/prowler/providers/aws/services/bedrock/bedrock_model_invocation_logs_encryption_enabled/bedrock_model_invocation_logs_encryption_enabled.metadata.json index 986f7310e1..703fde2d7f 100644 --- a/prowler/providers/aws/services/bedrock/bedrock_model_invocation_logs_encryption_enabled/bedrock_model_invocation_logs_encryption_enabled.metadata.json +++ b/prowler/providers/aws/services/bedrock/bedrock_model_invocation_logs_encryption_enabled/bedrock_model_invocation_logs_encryption_enabled.metadata.json @@ -1,27 +1,38 @@ { "Provider": "aws", "CheckID": "bedrock_model_invocation_logs_encryption_enabled", - "CheckTitle": "Ensure that Amazon Bedrock model invocation logs are encrypted with KMS.", - "CheckType": [], + "CheckTitle": "Amazon Bedrock model invocation logs are encrypted in the S3 bucket and KMS-encrypted in the CloudWatch log group", + "CheckType": [ + "Software and Configuration Checks/AWS Security Best Practices", + "Software and Configuration Checks/Industry and Regulatory Standards/AWS Foundational Security Best Practices", + "Software and Configuration Checks/Industry and Regulatory Standards/CIS AWS Foundations Benchmark", + "Effects/Data Exposure" + ], "ServiceName": "bedrock", "SubServiceName": "", - "ResourceIdTemplate": "arn:partition:bedrock:region:account-id:model/resource-id", + "ResourceIdTemplate": "", "Severity": "high", - "ResourceType": "Other", + "ResourceType": "AwsS3Bucket", "ResourceGroup": "ai_ml", - "Description": "Ensure that Amazon Bedrock model invocation logs are encrypted using AWS KMS to protect sensitive data in the request and response logs for all model invocations.", - "Risk": "If Amazon Bedrock model invocation logs are not encrypted, sensitive data such as prompts, responses, and token usage could be exposed to unauthorized parties. This may lead to data breaches, security vulnerabilities, or unintended use of sensitive information.", - "RelatedUrl": "https://docs.aws.amazon.com/bedrock/latest/userguide/data-protection.html", + "Description": "**Bedrock model invocation logs** are stored in encrypted destinations: **S3 buckets** with bucket encryption and **CloudWatch Logs** groups protected by an AWS KMS key.\n\nThis evaluates whether configured log targets enforce encryption at rest for request/response content and associated metadata.", + "Risk": "Without encryption at rest, prompts, outputs, images, and token/usage metadata in logs can be read if S3 or CloudWatch storage or replicas are accessed by unauthorized principals.\n\nImpacts:\n- Loss of data **confidentiality**\n- Secret or PII exposure enabling **account abuse**\n- Operational intel for **lateral movement**", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://docs.aws.amazon.com/bedrock/latest/userguide/data-protection.html", + "https://docs.aws.amazon.com/bedrock/latest/userguide/model-invocation-logging.html", + "https://support.icompaas.com/support/solutions/articles/62000233532-ensure-that-amazon-bedrock-model-invocation-logs-are-encrypted-with-kms", + "https://docs.aws.amazon.com/prescriptive-guidance/latest/patterns/configure-bedrock-invocation-logging-cloudformation.html" + ], "Remediation": { "Code": { "CLI": "", - "NativeIaC": "", - "Other": "", - "Terraform": "" + "NativeIaC": "```yaml\nResources:\n EncryptedLogsBucket:\n Type: AWS::S3::Bucket\n Properties:\n BucketName: \n BucketEncryption: # critical: enables default encryption for the bucket (SSE-S3)\n ServerSideEncryptionConfiguration:\n - ServerSideEncryptionByDefault:\n SSEAlgorithm: AES256\n\n EncryptedLogGroup:\n Type: AWS::Logs::LogGroup\n Properties:\n LogGroupName: \n KmsKeyId: # critical: encrypts the CloudWatch log group with a KMS key\n```", + "Other": "1. In the Bedrock console, go to Settings and note the S3 bucket and CloudWatch log group used for Model invocation logging.\n2. S3 bucket: AWS Console > S3 > Buckets > > Properties > Default encryption > Enable > Choose SSE-S3 (AES-256) > Save.\n3. CloudWatch Logs: AWS Console > CloudWatch > Logs > Log groups > select > Actions > Edit > KMS encryption > select > Save.", + "Terraform": "```hcl\nresource \"aws_s3_bucket\" \"example\" {\n bucket = \"\"\n}\n\nresource \"aws_s3_bucket_server_side_encryption_configuration\" \"example\" {\n bucket = aws_s3_bucket.example.id\n rule {\n apply_server_side_encryption_by_default {\n sse_algorithm = \"AES256\" # critical: enables default encryption for the S3 bucket\n }\n }\n}\n\nresource \"aws_cloudwatch_log_group\" \"example\" {\n name = \"\"\n kms_key_id = \"\" # critical: encrypts the log group with a KMS key\n}\n```" }, "Recommendation": { - "Text": "Ensure that model invocation logs for Amazon Bedrock are encrypted using AWS KMS to prevent unauthorized access to sensitive log data.", - "Url": "hhttps://docs.aws.amazon.com/bedrock/latest/userguide/data-protection.html" + "Text": "Ensure all invocation logs are encrypted end to end:\n- Enable S3 default encryption, preferably `SSE-KMS`, and restrict key usage\n- Assign a KMS key to CloudWatch log groups\n- Enforce **least privilege** on keys and logs, rotate keys, and monitor access for **defense in depth**", + "Url": "https://hub.prowler.com/check/bedrock_model_invocation_logs_encryption_enabled" } }, "Categories": [ diff --git a/prowler/providers/aws/services/datasync/datasync_task_logging_enabled/datasync_task_logging_enabled.metadata.json b/prowler/providers/aws/services/datasync/datasync_task_logging_enabled/datasync_task_logging_enabled.metadata.json index aea24cee9e..7c5d7cd05a 100644 --- a/prowler/providers/aws/services/datasync/datasync_task_logging_enabled/datasync_task_logging_enabled.metadata.json +++ b/prowler/providers/aws/services/datasync/datasync_task_logging_enabled/datasync_task_logging_enabled.metadata.json @@ -1,29 +1,34 @@ { "Provider": "aws", "CheckID": "datasync_task_logging_enabled", - "CheckTitle": "DataSync tasks should have logging enabled", + "CheckTitle": "DataSync task has CloudWatch Logs log group configured for logging", "CheckType": [ - "Software and Configuration Checks/AWS Security Best Practices" + "Software and Configuration Checks/AWS Security Best Practices", + "Software and Configuration Checks/Industry and Regulatory Standards/AWS Foundational Security Best Practices" ], "ServiceName": "datasync", "SubServiceName": "", - "ResourceIdTemplate": "arn:aws:datasync:{region}:{account-id}:task/{task-id}", + "ResourceIdTemplate": "", "Severity": "high", - "ResourceType": "AwsDataSyncTask", + "ResourceType": "Other", "ResourceGroup": "storage", - "Description": "This control checks if AWS DataSync tasks have logging enabled. The control fails if the task doesn't have the CloudWatchLogGroupArn property defined.", - "Risk": "Without logging enabled, important operational data may be lost, making it difficult to troubleshoot issues, monitor performance, and ensure compliance with auditing requirements.", - "RelatedUrl": "https://docs.aws.amazon.com/datasync/latest/userguide/monitor-datasync.html#enable-logging", + "Description": "**AWS DataSync tasks** are evaluated for a configured **CloudWatch Logs** destination (`CloudWatchLogGroupArn`).\n\nTasks that specify a log group are recognized as logging-enabled; those without one are identified as not publishing execution events.", + "Risk": "**Absent DataSync task logs** create blind spots, preventing timely detection of **failed or partial transfers**, unexpected deletions, or anomalies. This undermines data **integrity** verification, obscures potential **exfiltration** indicators, and slows forensics and recovery, reducing **availability** during incidents.", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://support.icompaas.com/support/solutions/articles/62000233637-ensure-datasync-tasks-should-have-logging-enabled", + "https://docs.aws.amazon.com/datasync/latest/userguide/monitor-datasync.html#enable-logging" + ], "Remediation": { "Code": { "CLI": "aws datasync update-task --task-arn --cloud-watch-log-group-arn ", - "NativeIaC": "", - "Other": "https://docs.aws.amazon.com/datasync/latest/userguide/monitor-datasync.html#enable-logging", - "Terraform": "" + "NativeIaC": "```yaml\n# CloudFormation: Enable CloudWatch Logs for a DataSync task\nResources:\n :\n Type: AWS::DataSync::Task\n Properties:\n SourceLocationArn: \n DestinationLocationArn: \n CloudWatchLogGroupArn: # Critical: attaches a CloudWatch Logs group to enable task logging\n```", + "Other": "1. In the AWS Console, go to DataSync > Tasks\n2. Select the task and click Edit\n3. In the Logging section, set CloudWatch Log group to an existing log group\n4. Click Save", + "Terraform": "```hcl\n# Enable CloudWatch Logs for a DataSync task\nresource \"aws_datasync_task\" \"\" {\n source_location_arn = \"\"\n destination_location_arn = \"\"\n cloudwatch_log_group_arn = \"\" # Critical: attaches a CloudWatch Logs group to enable task logging\n}\n```" }, "Recommendation": { - "Text": "Configure logging for your DataSync tasks to ensure that operational data is captured and available for debugging, monitoring, and auditing purposes.", - "Url": "https://docs.aws.amazon.com/datasync/latest/userguide/monitor-datasync.html#enable-logging" + "Text": "Configure each task to publish logs to a dedicated CloudWatch Logs group. Select an appropriate log level (e.g., `BASIC` or `TRANSFER`), enforce **least privilege** for log access, set **retention** and immutability, and integrate alerts. Centralize and monitor logs to support **defense in depth** and incident response.", + "Url": "https://hub.prowler.com/check/datasync_task_logging_enabled" } }, "Categories": [ diff --git a/prowler/providers/aws/services/dms/dms_instance_no_public_access/dms_instance_no_public_access.py b/prowler/providers/aws/services/dms/dms_instance_no_public_access/dms_instance_no_public_access.py index 833d5d1fb5..2b491529c9 100644 --- a/prowler/providers/aws/services/dms/dms_instance_no_public_access/dms_instance_no_public_access.py +++ b/prowler/providers/aws/services/dms/dms_instance_no_public_access/dms_instance_no_public_access.py @@ -25,8 +25,8 @@ class dms_instance_no_public_access(Check): if check_security_group( ingress_rule, "-1", - ports=None, any_address=True, + all_ports=True, ): report.status = "FAIL" report.status_extended = f"DMS Replication Instance {instance.id} is set as publicly accessible and security group {security_group.name} ({security_group.id}) is open to the Internet." diff --git a/prowler/providers/aws/services/ec2/ec2_securitygroup_allow_ingress_from_internet_to_any_port/ec2_securitygroup_allow_ingress_from_internet_to_any_port.py b/prowler/providers/aws/services/ec2/ec2_securitygroup_allow_ingress_from_internet_to_any_port/ec2_securitygroup_allow_ingress_from_internet_to_any_port.py index c4d3978199..f4ed7c20c4 100644 --- a/prowler/providers/aws/services/ec2/ec2_securitygroup_allow_ingress_from_internet_to_any_port/ec2_securitygroup_allow_ingress_from_internet_to_any_port.py +++ b/prowler/providers/aws/services/ec2/ec2_securitygroup_allow_ingress_from_internet_to_any_port/ec2_securitygroup_allow_ingress_from_internet_to_any_port.py @@ -31,7 +31,7 @@ class ec2_securitygroup_allow_ingress_from_internet_to_any_port(Check): report.status_extended = f"Security group {security_group.name} ({security_group.id}) does not have any port open to the Internet." for ingress_rule in security_group.ingress_rules: if check_security_group( - ingress_rule, "-1", ports=None, any_address=True + ingress_rule, "-1", any_address=True, all_ports=True ): self.check_enis( report=report, diff --git a/prowler/providers/aws/services/ec2/lib/security_groups.py b/prowler/providers/aws/services/ec2/lib/security_groups.py index a5f240adb5..dabbe4f86d 100644 --- a/prowler/providers/aws/services/ec2/lib/security_groups.py +++ b/prowler/providers/aws/services/ec2/lib/security_groups.py @@ -3,10 +3,14 @@ from typing import Any def check_security_group( - ingress_rule: Any, protocol: str, ports: list = [], any_address: bool = False + ingress_rule: Any, + protocol: str, + ports: list | None = None, + any_address: bool = False, + all_ports: bool = False, ) -> bool: """ - Check if the security group ingress rule has public access to the check_ports using the protocol + Check if the security group ingress rule has public access to the check_ports using the protocol. @param ingress_rule: AWS Security Group IpPermissions Ingress Rule { @@ -29,13 +33,17 @@ def check_security_group( @param protocol: Protocol to check. If -1, all protocols will be checked. - - @param ports: List of ports to check. If empty, any port will be checked. If None, any port will be checked. (Default: []) + @param ports: List of ports to check. If not provided all ports will be checked unless all_ports is False. (Default: None) @param any_address: If True, only 0.0.0.0/0 or "::/0" will be public and do not search for public addresses. (Default: False) + @param all_ports: If True, empty ports list will be treated as all ports. (Default: False) + @return: True if the security group has public access to the check_ports using the protocol """ + if ports is None: + ports = [] + # Check for all traffic ingress rules regardless of the protocol if ingress_rule["IpProtocol"] == "-1": for ip_ingress_rule in ingress_rule["IpRanges"]: @@ -54,54 +62,42 @@ def check_security_group( # Check for specific ports in ingress rules if "FromPort" in ingress_rule: - # If there is a port range + + # If the ports are not the same create a covering range. + # Note range is exclusive of the end value so we add 1 to the ToPort. if ingress_rule["FromPort"] != ingress_rule["ToPort"]: - # Calculate port range, adding 1 - diff = (ingress_rule["ToPort"] - ingress_rule["FromPort"]) + 1 - ingress_port_range = [] - for x in range(diff): - ingress_port_range.append(int(ingress_rule["FromPort"]) + x) - # If FromPort and ToPort are the same + ingress_port_range = set( + range(ingress_rule["FromPort"], ingress_rule["ToPort"] + 1) + ) else: - ingress_port_range = [] - ingress_port_range.append(int(ingress_rule["FromPort"])) + ingress_port_range = {int(ingress_rule["FromPort"])} - # Test Security Group - # IPv4 - for ip_ingress_rule in ingress_rule["IpRanges"]: - if _is_cidr_public(ip_ingress_rule["CidrIp"], any_address): - # If there are input ports to check - if ports: - for port in ports: - if ( - port in ingress_port_range - and ingress_rule["IpProtocol"] == protocol - ): - return True - # If empty input ports check if all ports are open - if len(set(ingress_port_range)) == 65536: - return True - # If None input ports check if any port is open - if ports is None: - return True + # Combine IPv4 and IPv6 ranges to facilitate a single check loop. + all_ingress_rules = [] + all_ingress_rules.extend(ingress_rule["IpRanges"]) + all_ingress_rules.extend(ingress_rule["Ipv6Ranges"]) - # IPv6 - for ip_ingress_rule in ingress_rule["Ipv6Ranges"]: - if _is_cidr_public(ip_ingress_rule["CidrIpv6"], any_address): - # If there are input ports to check - if ports: - for port in ports: - if ( - port in ingress_port_range - and ingress_rule["IpProtocol"] == protocol - ): - return True - # If empty input ports check if all ports are open - if len(set(ingress_port_range)) == 65536: - return True - # If None input ports check if any port is open - if ports is None: - return True + for ip_ingress_rule in all_ingress_rules: + # We only check public CIDRs + if _is_cidr_public( + ip_ingress_rule.get("CidrIp", ip_ingress_rule.get("CidrIpv6")), + any_address, + ): + for port in ports: + if port in ingress_port_range and ( + ingress_rule["IpProtocol"] == protocol or protocol == "-1" + ): + # Direct match for a port in the specified port range + return True + + # We did not find a specific port for the given protocol for + # a public cidr so let's see if all the ports are open + all_ports_open = len(ingress_port_range) == 65536 + + # Use the all_ports flag to determine if empty ports should be treated as all ports. + empty_ports_same_as_all_ports_open = all_ports and not ports + + return all_ports_open or empty_ports_same_as_all_ports_open return False @@ -120,3 +116,4 @@ def _is_cidr_public(cidr: str, any_address: bool = False) -> bool: return True if not any_address: return ipaddress.ip_network(cidr).is_global + return False diff --git a/prowler/providers/aws/services/opensearch/opensearch_service_domains_access_control_enabled/opensearch_service_domains_access_control_enabled.metadata.json b/prowler/providers/aws/services/opensearch/opensearch_service_domains_access_control_enabled/opensearch_service_domains_access_control_enabled.metadata.json index b67c47ae31..b2e07f1a59 100644 --- a/prowler/providers/aws/services/opensearch/opensearch_service_domains_access_control_enabled/opensearch_service_domains_access_control_enabled.metadata.json +++ b/prowler/providers/aws/services/opensearch/opensearch_service_domains_access_control_enabled/opensearch_service_domains_access_control_enabled.metadata.json @@ -1,30 +1,41 @@ { "Provider": "aws", "CheckID": "opensearch_service_domains_access_control_enabled", - "CheckTitle": "Check if Amazon Elasticsearch/Opensearch Service domains have fine grained access control enabled", - "CheckType": [], + "CheckTitle": "Amazon OpenSearch Service domain has fine-grained access control enabled", + "CheckType": [ + "Software and Configuration Checks/AWS Security Best Practices", + "Software and Configuration Checks/Industry and Regulatory Standards/AWS Foundational Security Best Practices" + ], "ServiceName": "opensearch", "SubServiceName": "", - "ResourceIdTemplate": "arn:partition:service:region:account-id:resource-id", + "ResourceIdTemplate": "", "Severity": "high", "ResourceType": "AwsOpenSearchServiceDomain", "ResourceGroup": "database", - "Description": "Check if Amazon Elasticsearch/Opensearch Service domains have fine grained access control enabled", - "Risk": "Amazon ES's fine graine access control enhances security by verifying that access to OpenSearch domains is controlled at a granular level, allowing for more precise permissions management and reducing the risk of unauthorised access.", - "RelatedUrl": "https://docs.aws.amazon.com/opensearch-service/latest/developerguide/fgac.html", + "Description": "**Amazon OpenSearch Service domains** are evaluated for **fine-grained access control** being enabled in `advanced-security-options`, ensuring role-based authorization at index, document, and field levels for API and Dashboards access.", + "Risk": "Without **fine-grained access control**, identities may gain overly broad permissions, enabling unauthorized reads or writes across indices and Dashboards. This undermines **confidentiality** and **integrity**, facilitates lateral movement, and increases the blast radius of a compromised account.", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://repost.aws/questions/QUvejSG0WDRByFVMcDchn_5w/how-do-resource-based-access-policies-interact-with-fgac-master-users-in-amazon-opensearch-service", + "https://docs.aws.amazon.com/securityhub/latest/userguide/opensearch-controls.html#opensearch-7", + "https://docs.aws.amazon.com/opensearch-service/latest/developerguide/fgac.html#fgac-enabling", + "https://ealtili.medium.com/how-to-use-fine-grained-access-control-in-amazon-opensearch-service-4dc86bffd40d" + ], "Remediation": { "Code": { - "CLI": "", - "NativeIaC": "", - "Other": "https://docs.aws.amazon.com/securityhub/latest/userguide/opensearch-controls.html#opensearch-7", - "Terraform": "" + "CLI": "aws opensearch update-domain-config --domain-name --advanced-security-options '{\"Enabled\":true,\"MasterUserOptions\":{\"MasterUserARN\":\"\"}}'", + "NativeIaC": "```yaml\n# CloudFormation: Enable fine-grained access control (FGAC) on an OpenSearch domain\nResources:\n :\n Type: AWS::OpenSearchService::Domain\n Properties:\n DomainName: \n AdvancedSecurityOptions:\n Enabled: true # Critical: Turns on FGAC\n MasterUserOptions:\n MasterUserARN: # Critical: Required to enable FGAC using an IAM principal\n```", + "Other": "1. In the AWS Console, go to Amazon OpenSearch Service\n2. Select your domain and choose Edit security configuration\n3. Enable Fine-grained access control\n4. Set the master user (choose IAM ARN and enter or create an internal master user)\n5. Save changes and wait for the update to complete", + "Terraform": "```hcl\n# Enable fine-grained access control (FGAC) on an OpenSearch domain\nresource \"aws_opensearch_domain\" \"\" {\n domain_name = \"\"\n\n advanced_security_options {\n enabled = true # Critical: Turns on FGAC\n master_user_options {\n master_user_arn = \"\" # Critical: Required to enable FGAC using an IAM principal\n }\n }\n}\n```" }, "Recommendation": { - "Text": "Enable fine grained access control for your OpenSearch domains", - "Url": "https://docs.aws.amazon.com/opensearch-service/latest/developerguide/fgac.html#fgac-enabling" + "Text": "Enable **fine-grained access control** in `advanced-security-options`. Define granular, role-based permissions (index/document/field) and map them to federated identities. Apply **least privilege**, deny-by-default, and **separation of duties**. Limit public access and regularly review role mappings.", + "Url": "https://hub.prowler.com/check/opensearch_service_domains_access_control_enabled" } }, - "Categories": [], + "Categories": [ + "identity-access" + ], "DependsOn": [], "RelatedTo": [], "Notes": "" diff --git a/prowler/providers/aws/services/opensearch/opensearch_service_domains_audit_logging_enabled/opensearch_service_domains_audit_logging_enabled.metadata.json b/prowler/providers/aws/services/opensearch/opensearch_service_domains_audit_logging_enabled/opensearch_service_domains_audit_logging_enabled.metadata.json index 593368a741..02b7bcf627 100644 --- a/prowler/providers/aws/services/opensearch/opensearch_service_domains_audit_logging_enabled/opensearch_service_domains_audit_logging_enabled.metadata.json +++ b/prowler/providers/aws/services/opensearch/opensearch_service_domains_audit_logging_enabled/opensearch_service_domains_audit_logging_enabled.metadata.json @@ -1,35 +1,38 @@ { "Provider": "aws", "CheckID": "opensearch_service_domains_audit_logging_enabled", - "CheckTitle": "Check if Amazon Elasticsearch/Opensearch Service domains have audit logging enabled", + "CheckTitle": "Amazon OpenSearch Service domain has audit logging enabled", "CheckType": [ - "Identify", - "Logging" + "Software and Configuration Checks/AWS Security Best Practices", + "Software and Configuration Checks/Industry and Regulatory Standards/AWS Foundational Security Best Practices" ], "ServiceName": "opensearch", "SubServiceName": "", - "ResourceIdTemplate": "arn:partition:service:region:account-id:resource-id", - "Severity": "low", + "ResourceIdTemplate": "", + "Severity": "high", "ResourceType": "AwsOpenSearchServiceDomain", "ResourceGroup": "database", - "Description": "Check if Amazon Elasticsearch/Opensearch Service domains have audit logging enabled", - "Risk": "If logs are not enabled, monitoring of service use and threat analysis is not possible.", + "Description": "**Amazon OpenSearch Service domains** have **audit logs** enabled via `AUDIT_LOGS`", + "Risk": "Without audit logs, critical actions lack accountability, reducing **confidentiality** and **integrity**.\n\nUnauthorized access, privilege misuse, and index tampering can go **undetected**, hindering **incident response** and **forensics**, and enabling data exfiltration and lateral movement without traceability.", "RelatedUrl": "", + "AdditionalURLs": [ + "https://docs.aws.amazon.com/elasticsearch-service/latest/developerguide/audit-logs.html" + ], "Remediation": { "Code": { - "CLI": "", - "NativeIaC": "", - "Other": "", - "Terraform": "" + "CLI": "aws opensearch update-domain-config --domain-name --log-publishing-options \"AUDIT_LOGS={CloudWatchLogsLogGroupArn=,Enabled=true}\"", + "NativeIaC": "```yaml\n# CloudFormation: Enable AUDIT_LOGS for an OpenSearch domain\nResources:\n OpenSearchDomain:\n Type: AWS::OpenSearchService::Domain\n Properties:\n DomainName: \n LogPublishingOptions:\n AUDIT_LOGS:\n CloudWatchLogsLogGroupArn: # Critical: where audit logs are sent\n Enabled: true # Critical: turns on AUDIT_LOGS to pass the check\n```", + "Other": "1. Open the AWS console and go to OpenSearch Service\n2. Select the domain \n3. Open the Logs tab, find Audit logs, and click Enable\n4. Choose or create a CloudWatch log group and confirm the resource policy if prompted\n5. Click Save changes to enable AUDIT_LOGS\n6. If Fine-grained access control is not enabled, enable it first, then repeat steps 3-5", + "Terraform": "```hcl\n# Enable AUDIT_LOGS for an OpenSearch domain\nresource \"aws_opensearch_domain\" \"example\" {\n domain_name = \"\"\n\n log_publishing_options {\n log_type = \"AUDIT_LOGS\"\n cloudwatch_log_group_arn = \"\" # Critical: destination for audit logs\n enabled = true # Critical: turns on AUDIT_LOGS to pass the check\n }\n}\n```" }, "Recommendation": { - "Text": "Make sure you are logging information about Amazon Elasticsearch Service operations.", - "Url": "https://docs.aws.amazon.com/elasticsearch-service/latest/developerguide/audit-logs.html" + "Text": "Enable `AUDIT_LOGS` for all domains and route them to a centralized, durable log store.\n\nTune categories to record auth failures and sensitive index operations. Apply **least privilege** to log access, enforce retention and immutability, and integrate alerts to provide **defense in depth** and timely response.", + "Url": "https://hub.prowler.com/check/opensearch_service_domains_audit_logging_enabled" } }, "Categories": [ - "forensics-ready", - "logging" + "logging", + "forensics-ready" ], "DependsOn": [], "RelatedTo": [], diff --git a/prowler/providers/aws/services/opensearch/opensearch_service_domains_cloudwatch_logging_enabled/opensearch_service_domains_cloudwatch_logging_enabled.metadata.json b/prowler/providers/aws/services/opensearch/opensearch_service_domains_cloudwatch_logging_enabled/opensearch_service_domains_cloudwatch_logging_enabled.metadata.json index 0349b64c19..8bdbd34b34 100644 --- a/prowler/providers/aws/services/opensearch/opensearch_service_domains_cloudwatch_logging_enabled/opensearch_service_domains_cloudwatch_logging_enabled.metadata.json +++ b/prowler/providers/aws/services/opensearch/opensearch_service_domains_cloudwatch_logging_enabled/opensearch_service_domains_cloudwatch_logging_enabled.metadata.json @@ -1,34 +1,44 @@ { "Provider": "aws", "CheckID": "opensearch_service_domains_cloudwatch_logging_enabled", - "CheckTitle": "Check if Amazon Elasticsearch/Opensearch Service domains have logging enabled", + "CheckTitle": "Amazon OpenSearch Service domain publishes search and index slow logs to CloudWatch Logs", "CheckType": [ - "Identify", - "Logging" + "Software and Configuration Checks/AWS Security Best Practices/Runtime Behavior Analysis", + "Software and Configuration Checks/Industry and Regulatory Standards/AWS Foundational Security Best Practices" ], "ServiceName": "opensearch", "SubServiceName": "", - "ResourceIdTemplate": "arn:partition:service:region:account-id:resource-id", - "Severity": "medium", + "ResourceIdTemplate": "", + "Severity": "low", "ResourceType": "AwsOpenSearchServiceDomain", "ResourceGroup": "database", - "Description": "Check if Amazon Elasticsearch/Opensearch Service domains have logging enabled", - "Risk": "Amazon ES exposes four Elasticsearch/Opensearch logs through Amazon CloudWatch Logs: error logs, search slow logs, index slow logs, and audit logs.", + "Description": "**Amazon OpenSearch Service** domains have **slow log publishing** enabled for both **search** and **indexing** operations to CloudWatch Logs (`SEARCH_SLOW_LOGS` and `INDEX_SLOW_LOGS`).", + "Risk": "Without these logs, visibility into **expensive searches** and **slow indexing** is lost, masking hotspots and abuse.\n- Availability: timeouts, throttling, node pressure\n- Integrity: missed or delayed indexing\n- Operations: slower incident response and capacity planning", "RelatedUrl": "", + "AdditionalURLs": [ + "https://docs.aws.amazon.com/elasticsearch-service/latest/developerguide/es-createdomain-configure-slow-logs.html", + "https://support.icompaas.com/support/solutions/articles/62000129471-ensure-amazon-elasticsearch-service-es-domains-have-logging-enabled", + "https://docs.prowler.com/checks/aws/elasticsearch-policies/elasticsearch_7", + "https://docs.prowler.com/checks/aws/elasticsearch-policies/elasticsearch_7#terraform", + "https://bigdataboutique.com/blog/inspecting-search-slow-logs-on-elasticsearch-and-opensearch-b05d87", + "https://repost.aws/knowledge-center/opensearch-troubleshoot-slow-logs", + "https://www.trendmicro.com/cloudoneconformity/knowledge-base/aws/Elasticsearch/slow-logs.html", + "https://medium.com/heyjobs-tech/how-to-create-an-opensearch-cluster-using-terraform-926b4a62b489", + "https://docs.aws.amazon.com/opensearch-service/latest/developerguide/createdomain-configure-slow-logs.html" + ], "Remediation": { "Code": { - "CLI": "aws logs put-resource-policy --policy-name --policy-document ", - "NativeIaC": "https://docs.prowler.com/checks/aws/elasticsearch-policies/elasticsearch_7#cloudformation", - "Other": "https://docs.prowler.com/checks/aws/elasticsearch-policies/elasticsearch_7", - "Terraform": "https://docs.prowler.com/checks/aws/elasticsearch-policies/elasticsearch_7#terraform" + "CLI": "aws opensearch update-domain-config --domain-name --log-publishing-options \"SEARCH_SLOW_LOGS={CloudWatchLogsLogGroupArn=,Enabled=true},INDEX_SLOW_LOGS={CloudWatchLogsLogGroupArn=,Enabled=true}\"", + "NativeIaC": "```yaml\n# CloudFormation: enable OpenSearch search and index slow logs to CloudWatch\nResources:\n :\n Type: AWS::OpenSearchService::Domain\n Properties:\n DomainName: \n LogPublishingOptions:\n SEARCH_SLOW_LOGS:\n CloudWatchLogsLogGroupArn: \n Enabled: true # Critical: enables SEARCH_SLOW_LOGS publishing\n INDEX_SLOW_LOGS:\n CloudWatchLogsLogGroupArn: \n Enabled: true # Critical: enables INDEX_SLOW_LOGS publishing\n```", + "Other": "1. In the AWS Console, open Amazon OpenSearch Service and select your domain\n2. Go to the Logs tab\n3. For Search slow logs, click Enable, choose or create a CloudWatch log group, accept/attach the suggested resource policy, then Save\n4. For Index slow logs, click Enable, choose or create a CloudWatch log group, accept/attach the suggested resource policy, then Save\n5. Wait for domain status to return to Active", + "Terraform": "```hcl\n# Enable OpenSearch search and index slow logs to CloudWatch\nresource \"aws_opensearch_domain\" \"\" {\n domain_name = \"\"\n\n # Critical: enables SEARCH_SLOW_LOGS publishing\n log_publishing_options {\n log_type = \"SEARCH_SLOW_LOGS\"\n cloudwatch_log_group_arn = \"\"\n enabled = true\n }\n\n # Critical: enables INDEX_SLOW_LOGS publishing\n log_publishing_options {\n log_type = \"INDEX_SLOW_LOGS\"\n cloudwatch_log_group_arn = \"\"\n enabled = true\n }\n}\n```" }, "Recommendation": { - "Text": "Enable Elasticsearch/Opensearch log. Create use cases for them. Using audit logs check for access denied events.", - "Url": "https://docs.aws.amazon.com/elasticsearch-service/latest/developerguide/es-createdomain-configure-slow-logs.html" + "Text": "Enable both `SEARCH_SLOW_LOGS` and `INDEX_SLOW_LOGS` for all domains and publish to CloudWatch. Set meaningful thresholds and retention, separate log groups, and alert on anomalies. Apply **least privilege** to log access and use **defense in depth** with complementary error and audit logs.", + "Url": "https://hub.prowler.com/check/opensearch_service_domains_cloudwatch_logging_enabled" } }, "Categories": [ - "forensics-ready", "logging" ], "DependsOn": [], diff --git a/prowler/providers/aws/services/opensearch/opensearch_service_domains_encryption_at_rest_enabled/opensearch_service_domains_encryption_at_rest_enabled.metadata.json b/prowler/providers/aws/services/opensearch/opensearch_service_domains_encryption_at_rest_enabled/opensearch_service_domains_encryption_at_rest_enabled.metadata.json index 8c7ae4260a..a896811296 100644 --- a/prowler/providers/aws/services/opensearch/opensearch_service_domains_encryption_at_rest_enabled/opensearch_service_domains_encryption_at_rest_enabled.metadata.json +++ b/prowler/providers/aws/services/opensearch/opensearch_service_domains_encryption_at_rest_enabled/opensearch_service_domains_encryption_at_rest_enabled.metadata.json @@ -1,31 +1,35 @@ { "Provider": "aws", "CheckID": "opensearch_service_domains_encryption_at_rest_enabled", - "CheckTitle": "Check if Amazon Elasticsearch/Opensearch Service domains have encryption at-rest enabled", + "CheckTitle": "Amazon OpenSearch Service domain has encryption at rest enabled", "CheckType": [ - "Protect", - "Data protection", - "Encryption of data at rest" + "Software and Configuration Checks/AWS Security Best Practices", + "Software and Configuration Checks/Industry and Regulatory Standards/AWS Foundational Security Best Practices", + "Effects/Data Exposure" ], "ServiceName": "opensearch", "SubServiceName": "", - "ResourceIdTemplate": "arn:partition:service:region:account-id:resource-id", - "Severity": "medium", + "ResourceIdTemplate": "", + "Severity": "critical", "ResourceType": "AwsOpenSearchServiceDomain", "ResourceGroup": "database", - "Description": "Check if Amazon Elasticsearch/Opensearch Service domains have encryption at-rest enabled", - "Risk": "If not enable unauthorized access to your data could risk increases.", + "Description": "**Amazon OpenSearch Service domains** are evaluated for `encryption at rest` using AWS KMS (`AES-256`) across stored data, including indexes, swap files, and automated snapshots.", + "Risk": "**Unencrypted OpenSearch data** can be read or copied if an attacker gains **disk-level access**, steals **automated snapshots**, or compromises the host. This jeopardizes **confidentiality** and enables tampering with stored indices, affecting **integrity**.", "RelatedUrl": "", + "AdditionalURLs": [ + "https://docs.aws.amazon.com/elasticsearch-service/latest/developerguide/encryption-at-rest.html", + "https://www.trendmicro.com/cloudoneconformity/knowledge-base/aws/Elasticsearch/encryption-at-rest.html" + ], "Remediation": { "Code": { - "CLI": "aws es update-elasticsearch-domain-config --domain-name --encryption-at-rest-options Enabled=true,KmsKeyId=", - "NativeIaC": "https://docs.prowler.com/checks/aws/elasticsearch-policies/elasticsearch_3-enable-encryptionatrest#cloudformation", - "Other": "https://www.trendmicro.com/cloudoneconformity/knowledge-base/aws/Elasticsearch/encryption-at-rest.html", - "Terraform": "" + "CLI": "aws opensearch update-domain-config --domain-name --encryption-at-rest-options Enabled=true", + "NativeIaC": "```yaml\n# CloudFormation: Enable encryption at rest for an OpenSearch domain\nResources:\n :\n Type: AWS::OpenSearchService::Domain\n Properties:\n DomainName: \n EncryptionAtRestOptions:\n Enabled: true # Critical: turns on encryption at rest for the domain\n```", + "Other": "1. In the AWS Console, go to OpenSearch Service > Domains and select your domain\n2. Click Actions > Edit security configuration\n3. Under Encryption, check Enable encryption of data at rest\n4. Keep the default AWS owned key (or select a KMS key if required)\n5. Click Save changes\n", + "Terraform": "```hcl\n# Terraform: Enable encryption at rest for an OpenSearch domain\nresource \"aws_opensearch_domain\" \"\" {\n domain_name = \"\"\n\n encrypt_at_rest {\n enabled = true # Critical: turns on encryption at rest for the domain\n }\n}\n```" }, "Recommendation": { - "Text": "Enable encryption at rest using AWS KMS to store and manage your encryption keys and the Advanced Encryption Standard algorithm with 256-bit keys (AES-256) to perform the encryption.", - "Url": "https://docs.aws.amazon.com/elasticsearch-service/latest/developerguide/encryption-at-rest.html" + "Text": "Enable `encryption at rest` with AWS KMS, preferably using **customer-managed keys**.\n- Enforce **least privilege** key policies and restrict grants\n- Enable automatic key rotation and monitor KMS usage\n- Encrypt logs and any exported snapshots\n- Apply **defense in depth** with network and IAM controls", + "Url": "https://hub.prowler.com/check/opensearch_service_domains_encryption_at_rest_enabled" } }, "Categories": [ diff --git a/prowler/providers/aws/services/opensearch/opensearch_service_domains_fault_tolerant_data_nodes/opensearch_service_domains_fault_tolerant_data_nodes.metadata.json b/prowler/providers/aws/services/opensearch/opensearch_service_domains_fault_tolerant_data_nodes/opensearch_service_domains_fault_tolerant_data_nodes.metadata.json index 613577f134..b933c979ca 100644 --- a/prowler/providers/aws/services/opensearch/opensearch_service_domains_fault_tolerant_data_nodes/opensearch_service_domains_fault_tolerant_data_nodes.metadata.json +++ b/prowler/providers/aws/services/opensearch/opensearch_service_domains_fault_tolerant_data_nodes/opensearch_service_domains_fault_tolerant_data_nodes.metadata.json @@ -1,33 +1,38 @@ { "Provider": "aws", "CheckID": "opensearch_service_domains_fault_tolerant_data_nodes", - "CheckTitle": "Ensure Elasticsearch/Opensearch domains have fault-tolerant data nodes.", + "CheckTitle": "OpenSearch domain has at least 3 data nodes and Zone Awareness enabled", "CheckType": [ - "Software and Configuration Checks/AWS Security Best Practices" + "Software and Configuration Checks/AWS Security Best Practices", + "Effects/Denial of Service" ], "ServiceName": "opensearch", "SubServiceName": "", - "ResourceIdTemplate": "arn:partition:es:{region}:{account-id}:domain/{domain-name}", + "ResourceIdTemplate": "", "Severity": "medium", - "ResourceType": "AwsElasticsearchDomain", + "ResourceType": "AwsOpenSearchServiceDomain", "ResourceGroup": "database", - "Description": "This control checks whether Elasticsearch/Opensearch domains are fault-tolerant with at least three data nodes and cross-zone replication (Zone Awareness) enabled.", - "Risk": "Without at least three data nodes and without cross-zone replication (Zone Awareness), the Elasticsearch/Opensearch domain may not be fault-tolerant, leading to a higher risk of data loss or unavailability in case of node failure.", - "RelatedUrl": "https://docs.aws.amazon.com/opensearch-service/latest/developerguide/what-is.html", + "Description": "**Amazon OpenSearch domains** are assessed for fault tolerance: **>= 3 data nodes** (`instance_count >= 3`) and **Zone Awareness** (`zone_awareness_enabled = true`) to distribute data across Availability Zones.", + "Risk": "**Insufficient data nodes** or disabled **Zone Awareness** reduces availability and durability. A node or AZ failure can trigger shard unavailability, write failures, or cluster outage, increasing risk of data inconsistency during rebalancing and blocking reads/writes until recovery.", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://docs.aws.amazon.com/opensearch-service/latest/developerguide/managedomains-multiaz.html", + "https://docs.aws.amazon.com/securityhub/latest/userguide/es-controls.html#es-6" + ], "Remediation": { "Code": { - "CLI": "aws opensearch update-domain-config --domain-name --cluster-config InstanceCount=3,ZoneAwarenessEnabled=true", - "NativeIaC": "", - "Other": "https://docs.aws.amazon.com/securityhub/latest/userguide/es-controls.html#es-6", - "Terraform": "" + "CLI": "aws opensearch update-domain-config --domain-name --cluster-config InstanceCount=3,ZoneAwarenessEnabled=true", + "NativeIaC": "```yaml\n# CloudFormation: Ensure at least 3 data nodes and enable Zone Awareness\nResources:\n :\n Type: AWS::OpenSearchService::Domain\n Properties:\n DomainName: \n ClusterConfig:\n InstanceType: m5.large.search\n InstanceCount: 3 # Critical: sets at least 3 data nodes for fault tolerance\n ZoneAwarenessEnabled: true # Critical: enables cross-AZ (Zone Awareness)\n```", + "Other": "1. Open the AWS Console and go to Amazon OpenSearch Service\n2. Select your domain and click Edit domain\n3. Under Cluster configuration:\n - Set Number of data nodes to 3 (or more)\n - Enable Zone awareness\n4. Click Submit to apply the changes", + "Terraform": "```hcl\n# Terraform: Ensure at least 3 data nodes and enable Zone Awareness\nresource \"aws_opensearch_domain\" \"\" {\n domain_name = \"\"\n\n cluster_config {\n instance_type = \"m5.large.search\"\n instance_count = 3 # Critical: sets at least 3 data nodes for fault tolerance\n zone_awareness_enabled = true # Critical: enables cross-AZ (Zone Awareness)\n }\n}\n```" }, "Recommendation": { - "Text": "Modify the Elasticsearch/Opensearch domain to ensure at least three data nodes and enable cross-zone replication (Zone Awareness) for high availability and fault tolerance.", - "Url": "https://docs.aws.amazon.com/opensearch-service/latest/developerguide/managedomains-multiaz.html" + "Text": "Configure OpenSearch with **>= 3 data nodes** and enable **Zone Awareness** to spread nodes across AZs.\n\n- Prefer Multi-AZ with Standby for resilient failover\n- Use node counts in multiples of three and set index replicas (`>= 1`)\n- Practice capacity planning and failure testing as **defense in depth**", + "Url": "https://hub.prowler.com/check/opensearch_service_domains_fault_tolerant_data_nodes" } }, "Categories": [ - "redundancy" + "resilience" ], "DependsOn": [], "RelatedTo": [], diff --git a/prowler/providers/aws/services/opensearch/opensearch_service_domains_fault_tolerant_master_nodes/opensearch_service_domains_fault_tolerant_master_nodes.metadata.json b/prowler/providers/aws/services/opensearch/opensearch_service_domains_fault_tolerant_master_nodes/opensearch_service_domains_fault_tolerant_master_nodes.metadata.json index dab0fac85e..9525e0c5eb 100644 --- a/prowler/providers/aws/services/opensearch/opensearch_service_domains_fault_tolerant_master_nodes/opensearch_service_domains_fault_tolerant_master_nodes.metadata.json +++ b/prowler/providers/aws/services/opensearch/opensearch_service_domains_fault_tolerant_master_nodes/opensearch_service_domains_fault_tolerant_master_nodes.metadata.json @@ -1,31 +1,38 @@ { "Provider": "aws", "CheckID": "opensearch_service_domains_fault_tolerant_master_nodes", - "CheckTitle": "OpenSearch Service Domain should have at least three dedicated master nodes", - "CheckType": [], + "CheckTitle": "OpenSearch domain has at least 3 dedicated master nodes", + "CheckType": [ + "Software and Configuration Checks/AWS Security Best Practices", + "Effects/Denial of Service" + ], "ServiceName": "opensearch", "SubServiceName": "", - "ResourceIdTemplate": "arn:partition:es:region:account-id:domain/resource-id", + "ResourceIdTemplate": "", "Severity": "medium", "ResourceType": "AwsOpenSearchServiceDomain", "ResourceGroup": "database", - "Description": "OpenSearch Service uses dedicated master nodes to increase cluster stability. A minimum of three dedicated master nodes is recommended to ensure high availability.", - "Risk": "If a master node fails, the cluster may become unavailable.", - "RelatedUrl": "https://docs.aws.amazon.com/opensearch-service/latest/developerguide/managedomains-dedicatedmasternodes.html#dedicatedmasternodes-number", + "Description": "**Amazon OpenSearch domains** have **dedicated master nodes** enabled with a master node count of at least `3` to support stable cluster coordination and elections", + "Risk": "With fewer than `3` or disabled **dedicated master nodes**, the cluster can lose **quorum**, blocking leader election.\n\nEffects include stalled cluster state updates, failed reads/writes, shard allocation issues, and possible split-brain, reducing **availability** and **integrity**.", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://docs.aws.amazon.com/securityhub/latest/userguide/opensearch-controls.html#opensearch-11", + "https://docs.aws.amazon.com/opensearch-service/latest/developerguide/managedomains-dedicatedmasternodes.html#dedicatedmasternodes-number" + ], "Remediation": { "Code": { - "CLI": "aws es update-elasticsearch-domain-config --region --domain-name --elasticsearch-cluster-config DedicatedMasterEnabled=true,DedicatedMasterType='',DedicatedMasterCount=3", - "NativeIaC": "", - "Other": "https://docs.aws.amazon.com/securityhub/latest/userguide/opensearch-controls.html#opensearch-11", - "Terraform": "" + "CLI": "aws opensearch update-domain-config --domain-name --cluster-config \"DedicatedMasterEnabled=true,DedicatedMasterType=,DedicatedMasterCount=3\"", + "NativeIaC": "```yaml\n# CloudFormation: set at least 3 dedicated master nodes\nResources:\n :\n Type: AWS::OpenSearchService::Domain\n Properties:\n ClusterConfig:\n DedicatedMasterEnabled: true # Critical: enable dedicated master nodes\n DedicatedMasterCount: 3 # Critical: ensure minimum of 3 masters\n DedicatedMasterType: \"\" # Critical: required when enabling masters\n```", + "Other": "1. Sign in to the AWS Console and open Amazon OpenSearch Service\n2. Select your domain and choose Edit\n3. In Cluster configuration:\n - Enable Dedicated master nodes\n - Set Dedicated master node count to 3\n - Select a Dedicated master instance type\n4. Choose Save changes", + "Terraform": "```hcl\n# Terraform: set at least 3 dedicated master nodes\nresource \"aws_opensearch_domain\" \"\" {\n domain_name = \"\"\n\n cluster_config {\n dedicated_master_enabled = true # Critical: enable dedicated masters\n dedicated_master_count = 3 # Critical: ensure minimum of 3 masters\n dedicated_master_type = \"\" # Critical: required when enabling masters\n }\n}\n```" }, "Recommendation": { - "Text": "Ensure that your OpenSearch Service domain has at least three dedicated master nodes", - "Url": "https://docs.aws.amazon.com/opensearch-service/latest/developerguide/managedomains-dedicatedmasternodes.html#dedicatedmasternodes-number" + "Text": "Enable **dedicated master nodes** and set the count to at least `3` (use an odd number) to maintain **quorum**. Use *Multi-AZ with standby* to distribute masters across zones. Right-size master instances and monitor cluster health to uphold high availability and resilience.", + "Url": "https://hub.prowler.com/check/opensearch_service_domains_fault_tolerant_master_nodes" } }, "Categories": [ - "redundancy" + "resilience" ], "DependsOn": [], "RelatedTo": [], diff --git a/prowler/providers/aws/services/opensearch/opensearch_service_domains_https_communications_enforced/opensearch_service_domains_https_communications_enforced.metadata.json b/prowler/providers/aws/services/opensearch/opensearch_service_domains_https_communications_enforced/opensearch_service_domains_https_communications_enforced.metadata.json index 7c6f83ebe2..83fd59f167 100644 --- a/prowler/providers/aws/services/opensearch/opensearch_service_domains_https_communications_enforced/opensearch_service_domains_https_communications_enforced.metadata.json +++ b/prowler/providers/aws/services/opensearch/opensearch_service_domains_https_communications_enforced/opensearch_service_domains_https_communications_enforced.metadata.json @@ -1,31 +1,35 @@ { "Provider": "aws", "CheckID": "opensearch_service_domains_https_communications_enforced", - "CheckTitle": "Check if Amazon Elasticsearch/Opensearch Service domains have enforce HTTPS enabled", + "CheckTitle": "OpenSearch domain has HTTPS enforcement enabled", "CheckType": [ - "Protect", - "Data protection", - "Encryption of data in transit" + "Software and Configuration Checks/AWS Security Best Practices", + "Software and Configuration Checks/Industry and Regulatory Standards/AWS Foundational Security Best Practices", + "Software and Configuration Checks/Industry and Regulatory Standards/NIST 800-53 Controls (USA)", + "Software and Configuration Checks/Industry and Regulatory Standards/PCI-DSS" ], "ServiceName": "opensearch", "SubServiceName": "", - "ResourceIdTemplate": "arn:partition:service:region:account-id:resource-id", - "Severity": "medium", + "ResourceIdTemplate": "", + "Severity": "high", "ResourceType": "AwsOpenSearchServiceDomain", "ResourceGroup": "database", - "Description": "Check if Amazon Elasticsearch/Opensearch Service domains have enforce HTTPS enabled", - "Risk": "If not enable unauthorized access to your data could risk increases.", + "Description": "Amazon OpenSearch Service domains with **HTTPS enforcement** require encrypted connections. This assessment identifies domains missing `Require HTTPS for all traffic`, indicating that unencrypted HTTP is accepted.", + "Risk": "Allowing HTTP exposes queries, credentials, and results in cleartext, enabling interception and session hijacking. Adversaries can alter requests or responses, compromising **confidentiality** and **integrity**, and harvest auth data for **lateral movement**.", "RelatedUrl": "", + "AdditionalURLs": [ + "https://docs.aws.amazon.com/elasticsearch-service/latest/developerguide/es-createupdatedomains.html" + ], "Remediation": { "Code": { - "CLI": "", - "NativeIaC": "https://docs.prowler.com/checks/aws/elasticsearch-policies/elasticsearch_6#fix---builtime", - "Other": "https://docs.prowler.com/checks/aws/elasticsearch-policies/elasticsearch_6#aws-console", - "Terraform": "" + "CLI": "aws opensearch update-domain-config --domain-name --domain-endpoint-options EnforceHTTPS=true", + "NativeIaC": "```yaml\n# CloudFormation - Enable HTTPS enforcement on an OpenSearch domain\nResources:\n :\n Type: AWS::OpenSearchService::Domain\n Properties:\n DomainEndpointOptions:\n EnforceHTTPS: true # Critical: requires all traffic to use HTTPS, fixing the finding\n```", + "Other": "1. Open the Amazon OpenSearch Service console\n2. Go to Domains and select your domain\n3. Click Actions > Edit security configuration\n4. Check \"Require HTTPS for all traffic to the domain\"\n5. Click Save changes", + "Terraform": "```hcl\n# Enable HTTPS enforcement on an OpenSearch domain\nresource \"aws_opensearch_domain\" \"\" {\n domain_name = \"\"\n\n domain_endpoint_options {\n enforce_https = true # Critical: requires HTTPS for all requests\n }\n}\n```" }, "Recommendation": { - "Text": "When creating ES Domains, enable 'Require HTTPS fo all traffic to the domain'", - "Url": "https://docs.aws.amazon.com/elasticsearch-service/latest/developerguide/es-createupdatedomains.html" + "Text": "Enable `Require HTTPS for all traffic` on every domain to enforce TLS. Prefer strong protocols (TLS 1.2+), and block HTTP via network controls for defense in depth. Apply **least privilege** access policies and use private connectivity to minimize exposure and downgrade risks.", + "Url": "https://hub.prowler.com/check/opensearch_service_domains_https_communications_enforced" } }, "Categories": [ diff --git a/prowler/providers/aws/services/opensearch/opensearch_service_domains_internal_user_database_enabled/opensearch_service_domains_internal_user_database_enabled.metadata.json b/prowler/providers/aws/services/opensearch/opensearch_service_domains_internal_user_database_enabled/opensearch_service_domains_internal_user_database_enabled.metadata.json index 73794fa8e2..cd127fe4b2 100644 --- a/prowler/providers/aws/services/opensearch/opensearch_service_domains_internal_user_database_enabled/opensearch_service_domains_internal_user_database_enabled.metadata.json +++ b/prowler/providers/aws/services/opensearch/opensearch_service_domains_internal_user_database_enabled/opensearch_service_domains_internal_user_database_enabled.metadata.json @@ -1,33 +1,38 @@ { "Provider": "aws", "CheckID": "opensearch_service_domains_internal_user_database_enabled", - "CheckTitle": "Check if Amazon Elasticsearch/Opensearch Service domains have internal user database enabled", + "CheckTitle": "Amazon OpenSearch Service domain has internal user database disabled", "CheckType": [ - "Protect", - "Data protection" + "Software and Configuration Checks/AWS Security Best Practices" ], "ServiceName": "opensearch", "SubServiceName": "", - "ResourceIdTemplate": "arn:partition:service:region:account-id:resource-id", + "ResourceIdTemplate": "", "Severity": "medium", "ResourceType": "AwsOpenSearchServiceDomain", "ResourceGroup": "database", - "Description": "Check if Amazon Elasticsearch/Opensearch Service domains have internal user database enabled", - "Risk": "Internal User Database is convenient for demos, for production environment use Federated authentication.", + "Description": "**Amazon OpenSearch Service domains** are evaluated for the **internal user database** setting (`InternalUserDatabaseEnabled`). The finding identifies domains that rely on built-in HTTP basic users instead of external identity providers.", + "Risk": "An enabled internal user database creates **credential sprawl** and weak **account lifecycle**. Missing centralized MFA, rotation, and revocation raises unauthorized access risk, impacting **confidentiality** and **integrity**.\n\nBasic auth on exposed endpoints eases brute force and reduces **auditability**.", "RelatedUrl": "", + "AdditionalURLs": [ + "https://docs.aws.amazon.com/elasticsearch-service/latest/developerguide/fgac.html", + "https://docs.aws.amazon.com/opensearch-service/latest/developerguide/fgac.html" + ], "Remediation": { "Code": { - "CLI": "", - "NativeIaC": "", - "Other": "", - "Terraform": "" + "CLI": "aws opensearch update-domain-config --domain-name --advanced-security-options '{\"InternalUserDatabaseEnabled\":false}'", + "NativeIaC": "```yaml\n# CloudFormation: disable internal user database for the domain\nResources:\n :\n Type: AWS::OpenSearchService::Domain\n Properties:\n AdvancedSecurityOptions:\n InternalUserDatabaseEnabled: false # Critical: disables internal user DB to pass the check\n```", + "Other": "1. In AWS console, go to Amazon OpenSearch Service > Domains\n2. Select the domain and choose Edit security configuration\n3. Under Fine-grained access control, turn off Internal user database\n4. Click Save changes", + "Terraform": "```hcl\n# Terraform: disable internal user database for the domain\nresource \"aws_opensearch_domain\" \"\" {\n domain_name = \"\"\n\n advanced_security_options {\n internal_user_database_enabled = false # Critical: disables internal user DB to pass the check\n }\n}\n```" }, "Recommendation": { - "Text": "Remove users from internal user database and uso Cognito instead.", - "Url": "https://docs.aws.amazon.com/elasticsearch-service/latest/developerguide/fgac.html" + "Text": "Prefer **federated authentication** (IAM, SAML, or Amazon Cognito) and disable the **internal user database**. Enforce **least privilege** roles, require **MFA**, centralize credential rotation and offboarding, and log access. Use **VPC access** and restrictive policies; avoid HTTP basic auth to minimize exposure.", + "Url": "https://hub.prowler.com/check/opensearch_service_domains_internal_user_database_enabled" } }, - "Categories": [], + "Categories": [ + "identity-access" + ], "DependsOn": [], "RelatedTo": [], "Notes": "" diff --git a/prowler/providers/aws/services/opensearch/opensearch_service_domains_node_to_node_encryption_enabled/opensearch_service_domains_node_to_node_encryption_enabled.metadata.json b/prowler/providers/aws/services/opensearch/opensearch_service_domains_node_to_node_encryption_enabled/opensearch_service_domains_node_to_node_encryption_enabled.metadata.json index bcb56c817f..c81ae70bcd 100644 --- a/prowler/providers/aws/services/opensearch/opensearch_service_domains_node_to_node_encryption_enabled/opensearch_service_domains_node_to_node_encryption_enabled.metadata.json +++ b/prowler/providers/aws/services/opensearch/opensearch_service_domains_node_to_node_encryption_enabled/opensearch_service_domains_node_to_node_encryption_enabled.metadata.json @@ -1,31 +1,38 @@ { "Provider": "aws", "CheckID": "opensearch_service_domains_node_to_node_encryption_enabled", - "CheckTitle": "Check if Amazon Elasticsearch/Opensearch Service domains have node-to-node encryption enabled", + "CheckTitle": "Amazon OpenSearch Service domain has node-to-node encryption enabled", "CheckType": [ - "Protect", - "Data protection", - "Encryption of data in transit" + "Software and Configuration Checks/AWS Security Best Practices", + "Software and Configuration Checks/Industry and Regulatory Standards/AWS Foundational Security Best Practices", + "Software and Configuration Checks/Industry and Regulatory Standards/NIST CSF Controls (USA)", + "Software and Configuration Checks/Industry and Regulatory Standards/NIST 800-53 Controls (USA)", + "Software and Configuration Checks/Industry and Regulatory Standards/PCI-DSS" ], "ServiceName": "opensearch", "SubServiceName": "", - "ResourceIdTemplate": "arn:partition:service:region:account-id:resource-id", - "Severity": "medium", + "ResourceIdTemplate": "", + "Severity": "high", "ResourceType": "AwsOpenSearchServiceDomain", "ResourceGroup": "database", - "Description": "Check if Amazon Elasticsearch/Opensearch Service domains have node-to-node encryption enabled", - "Risk": "Node-to-node encryption provides an additional layer of security on top of the default features of Amazon ES. This architecture prevents potential attackers from intercepting traffic between Elasticsearch nodes and keeps the cluster secure.", + "Description": "**Amazon OpenSearch domains** with **node-to-node encryption** use TLS to protect traffic between cluster nodes. The finding evaluates the domain's `node_to_node_encryption` configuration for intra-cluster communications.", + "Risk": "Unencrypted intra-cluster traffic enables interception and manipulation by anyone with network foothold.\n- **Confidentiality**: exposure of documents, credentials, metadata\n- **Integrity**: tampering with queries and shard replication\n- **Availability**: spoofing/MITM can disrupt coordination and cause outages", "RelatedUrl": "", + "AdditionalURLs": [ + "https://docs.aws.amazon.com/elasticsearch-service/latest/developerguide/ntn.html", + "https://docs.aws.amazon.com/opensearch-service/latest/developerguide/ntn.html", + "https://www.trendmicro.com/cloudoneconformity/knowledge-base/aws/Elasticsearch/node-to-node-encryption.html" + ], "Remediation": { "Code": { - "CLI": "aws es update-elasticsearch-domain-config --domain-name --node-to-node-encryption-options Enabled=true", - "NativeIaC": "https://docs.prowler.com/checks/aws/elasticsearch-policies/elasticsearch_5#cloudformation", - "Other": "https://www.trendmicro.com/cloudoneconformity/knowledge-base/aws/Elasticsearch/node-to-node-encryption.html", - "Terraform": "" + "CLI": "aws opensearchservice update-domain-config --domain-name --node-to-node-encryption-options Enabled=true", + "NativeIaC": "```yaml\n# CloudFormation: Enable node-to-node encryption for an OpenSearch domain\nResources:\n OpenSearchDomain:\n Type: AWS::OpenSearchService::Domain\n Properties:\n NodeToNodeEncryptionOptions:\n Enabled: true # Critical: enables TLS between nodes to pass the check\n```", + "Other": "1. In the AWS Console, go to OpenSearch Service > Domains\n2. Select the target domain\n3. Click Edit (or Actions > Edit security configuration)\n4. Under Encryption, enable Node-to-node encryption\n5. Click Save changes", + "Terraform": "```hcl\n# Terraform: Enable node-to-node encryption for an OpenSearch domain\nresource \"aws_opensearch_domain\" \"\" {\n domain_name = \"\"\n\n node_to_node_encryption {\n enabled = true # Critical: encrypts intra-cluster traffic to pass the check\n }\n}\n```" }, "Recommendation": { - "Text": "Node-to-node encryption on new domains requires Elasticsearch 6.0 or later. Enabling the feature on existing domains requires Elasticsearch 6.7 or later. Choose the existing domain in the AWS console, Actions, and Modify encryption.", - "Url": "https://docs.aws.amazon.com/elasticsearch-service/latest/developerguide/ntn.html" + "Text": "Enable **node-to-node encryption** (`node_to_node_encryption: true`) to enforce TLS for inter-node traffic. Apply **defense in depth**: require HTTPS for clients, restrict network exposure, and use least privilege. Validate performance in staging and plan carefully, as the setting is effectively irreversible.", + "Url": "https://hub.prowler.com/check/opensearch_service_domains_node_to_node_encryption_enabled" } }, "Categories": [ diff --git a/prowler/providers/aws/services/opensearch/opensearch_service_domains_not_publicly_accessible/opensearch_service_domains_not_publicly_accessible.metadata.json b/prowler/providers/aws/services/opensearch/opensearch_service_domains_not_publicly_accessible/opensearch_service_domains_not_publicly_accessible.metadata.json index 53dd75b6ee..1ca70cc0d8 100644 --- a/prowler/providers/aws/services/opensearch/opensearch_service_domains_not_publicly_accessible/opensearch_service_domains_not_publicly_accessible.metadata.json +++ b/prowler/providers/aws/services/opensearch/opensearch_service_domains_not_publicly_accessible/opensearch_service_domains_not_publicly_accessible.metadata.json @@ -1,29 +1,36 @@ { "Provider": "aws", "CheckID": "opensearch_service_domains_not_publicly_accessible", - "CheckTitle": "Check if Amazon Opensearch/Elasticsearch domains are publicly accessible", + "CheckTitle": "Amazon OpenSearch Service domain is not publicly accessible", "CheckType": [ - "Effects/Data Exposure" + "Software and Configuration Checks/AWS Security Best Practices/Network Reachability", + "Software and Configuration Checks/Industry and Regulatory Standards/AWS Foundational Security Best Practices", + "Effects/Data Exposure", + "TTPs/Initial Access" ], "ServiceName": "opensearch", "SubServiceName": "", - "ResourceIdTemplate": "arn:partition:service:region:account-id:resource-id", + "ResourceIdTemplate": "", "Severity": "critical", "ResourceType": "AwsOpenSearchServiceDomain", "ResourceGroup": "database", - "Description": "Check if Amazon Opensearch/Elasticsearch domains are publicly accessible via their access policies.", - "Risk": "Publicly accessible services could expose sensitive data to bad actors.", + "Description": "**Amazon OpenSearch domains** are assessed for **public exposure** via their resource-based access policies. Domains inside a VPC are treated as **privately reachable**; domains with overly permissive policies that allow broad, unauthenticated access are identified as **publicly accessible**.", + "Risk": "Public exposure lets anyone query, index, or delete data, impacting **confidentiality** (record disclosure), **integrity** (unauthorized writes, index tampering), and **availability** (disruption, deletion). Attackers can harvest sensitive logs/PII, alter analytics, or wipe indices, enabling lateral movement and operational outage.", "RelatedUrl": "", + "AdditionalURLs": [ + "https://docs.aws.amazon.com/elasticsearch-service/latest/developerguide/es-vpc.html", + "https://www.trendmicro.com/cloudoneconformity/knowledge-base/aws/Elasticsearch/domain-exposed.html" + ], "Remediation": { "Code": { - "CLI": "", - "NativeIaC": "", - "Other": "https://www.trendmicro.com/cloudoneconformity/knowledge-base/aws/Elasticsearch/domain-exposed.html", - "Terraform": "" + "CLI": "aws opensearch update-domain-config --domain-name --access-policies '{\"Version\":\"2012-10-17\",\"Statement\":[{\"Effect\":\"Allow\",\"Principal\":{\"AWS\":\"arn:aws:iam:::root\"},\"Action\":\"es:*\",\"Resource\":\"arn:aws:es:::domain//*\"}]}'", + "NativeIaC": "```yaml\n# CloudFormation: restrict OpenSearch access policy to your account only\nResources:\n :\n Type: AWS::OpenSearchService::Domain\n Properties:\n DomainName: \n AccessPolicies: # critical: restricts access to your account only, removing public access\n Version: '2012-10-17'\n Statement:\n - Effect: Allow\n Principal:\n AWS: arn:aws:iam:::root # critical: only this account can access\n Action: es:*\n Resource: arn:aws:es:::domain//*\n```", + "Other": "1. In the AWS console, open Amazon OpenSearch Service and select your domain\n2. Go to Security configuration > Edit\n3. Choose Access policy > JSON\n4. Replace the policy with the following (use your values) and Save changes:\n```json\n{\n \"Version\": \"2012-10-17\",\n \"Statement\": [\n {\n \"Effect\": \"Allow\",\n \"Principal\": {\"AWS\": \"arn:aws:iam:::root\"},\n \"Action\": \"es:*\",\n \"Resource\": \"arn:aws:es:::domain//*\"\n }\n ]\n}\n```\n5. Verify the domain endpoint is no longer accessible publicly except by your account's IAM principals", + "Terraform": "```hcl\n# Restrict OpenSearch access policy to your account only\nresource \"aws_opensearch_domain\" \"\" {\n domain_name = \"\"\n\n # critical: limits access to the owning account, removing public access\n access_policies = jsonencode({\n Version = \"2012-10-17\"\n Statement = [{\n Effect = \"Allow\"\n Principal = { AWS = \"arn:aws:iam:::root\" }\n Action = \"es:*\"\n Resource = \"arn:aws:es:::domain//*\"\n }]\n })\n}\n```" }, "Recommendation": { - "Text": "Modify the access policy attached to your Amazon OpenSearch domain and replace the 'Principal' element value (i.e. '*') with the ARN of the trusted AWS account. You can also add a Condition clause to the policy statement to limit the domain access to a specific (trusted) IP address/IP range only.", - "Url": "https://docs.aws.amazon.com/elasticsearch-service/latest/developerguide/es-vpc.html" + "Text": "Apply **least privilege** and **defense in depth**:\n- Place domains in a **VPC** and restrict reachability with security groups\n- Use narrow resource policies; avoid `Principal:\"*\"`\n- Require authenticated access (fine-grained controls); *if unavoidable*, limit public endpoints by IP and roles\n- Monitor access with logs and alerts", + "Url": "https://hub.prowler.com/check/opensearch_service_domains_not_publicly_accessible" } }, "Categories": [ diff --git a/prowler/providers/aws/services/opensearch/opensearch_service_domains_updated_to_the_latest_service_software_version/opensearch_service_domains_updated_to_the_latest_service_software_version.metadata.json b/prowler/providers/aws/services/opensearch/opensearch_service_domains_updated_to_the_latest_service_software_version/opensearch_service_domains_updated_to_the_latest_service_software_version.metadata.json index 100a5d995e..65fa95121d 100644 --- a/prowler/providers/aws/services/opensearch/opensearch_service_domains_updated_to_the_latest_service_software_version/opensearch_service_domains_updated_to_the_latest_service_software_version.metadata.json +++ b/prowler/providers/aws/services/opensearch/opensearch_service_domains_updated_to_the_latest_service_software_version/opensearch_service_domains_updated_to_the_latest_service_software_version.metadata.json @@ -1,33 +1,40 @@ { "Provider": "aws", "CheckID": "opensearch_service_domains_updated_to_the_latest_service_software_version", - "CheckTitle": "Check if Amazon Elasticsearch/Opensearch Service domains have updates available", + "CheckTitle": "Amazon OpenSearch Service domain is updated to the latest service software version", "CheckType": [ - "Detect", - "Vulnerability, patch, and version management" + "Software and Configuration Checks/Patch Management", + "Software and Configuration Checks/AWS Security Best Practices" ], "ServiceName": "opensearch", "SubServiceName": "", - "ResourceIdTemplate": "arn:partition:service:region:account-id:resource-id", - "Severity": "low", + "ResourceIdTemplate": "", + "Severity": "high", "ResourceType": "AwsOpenSearchServiceDomain", "ResourceGroup": "database", - "Description": "Check if Amazon Elasticsearch/Opensearch Service domains have updates available", - "Risk": "Amazon ES regularly releases system software updates that add features or otherwise improve your domains.", + "Description": "**OpenSearch Service domains** are assessed for pending **service software updates**. This focuses on internal platform updates, distinct from engine version upgrades.", + "Risk": "**Missing service software updates** can leave known flaws unpatched, threatening data confidentiality and index integrity.\n\nRequired updates missed may lead to AWS isolating the domain, causing **outages** and, if prolonged, **permanent deletion**.", "RelatedUrl": "", + "AdditionalURLs": [ + "https://docs.aws.amazon.com/opensearch-service/latest/developerguide/service-software.html", + "https://www.trendmicro.com/cloudoneconformity/knowledge-base/aws/Elasticsearch/version.html", + "https://docs.aws.amazon.com/elasticsearch-service/latest/developerguide/es-service-software.html" + ], "Remediation": { "Code": { - "CLI": "aws es upgrade-elasticsearch-domain --domain-name --target-version --perform-check-only", + "CLI": "aws opensearch start-service-software-update --domain-name ", "NativeIaC": "", - "Other": "https://www.trendmicro.com/cloudoneconformity/knowledge-base/aws/Elasticsearch/version.html", + "Other": "1. Sign in to the AWS Console and open Amazon OpenSearch Service\n2. Select the target domain\n3. Click Actions > Update\n4. Choose Apply update now\n5. Click Confirm to start the service software update", "Terraform": "" }, "Recommendation": { - "Text": "The Notifications panel in the console is the easiest way to see if an update is available or check the status of an update. You can also receive these notifications through Amazon EventBridge. If you take no action on required updates, Amazon ES still updates your domain service software automatically after a certain timeframe (typically two weeks). In this situation, Amazon ES sends notifications when it starts the update and when the update is complete.", - "Url": "https://docs.aws.amazon.com/elasticsearch-service/latest/developerguide/es-service-software.html" + "Text": "Apply the latest **service software updates** promptly. Schedule updates during the domain's **off-peak window** or enable automatic updates. Monitor console or **EventBridge** notifications, and test changes in staging to support **defense in depth** while minimizing downtime.", + "Url": "https://hub.prowler.com/check/opensearch_service_domains_updated_to_the_latest_service_software_version" } }, - "Categories": [], + "Categories": [ + "vulnerabilities" + ], "DependsOn": [], "RelatedTo": [], "Notes": "" diff --git a/prowler/providers/aws/services/opensearch/opensearch_service_domains_use_cognito_authentication_for_kibana/opensearch_service_domains_use_cognito_authentication_for_kibana.metadata.json b/prowler/providers/aws/services/opensearch/opensearch_service_domains_use_cognito_authentication_for_kibana/opensearch_service_domains_use_cognito_authentication_for_kibana.metadata.json index 9a5c2c8ebe..c144c1d356 100644 --- a/prowler/providers/aws/services/opensearch/opensearch_service_domains_use_cognito_authentication_for_kibana/opensearch_service_domains_use_cognito_authentication_for_kibana.metadata.json +++ b/prowler/providers/aws/services/opensearch/opensearch_service_domains_use_cognito_authentication_for_kibana/opensearch_service_domains_use_cognito_authentication_for_kibana.metadata.json @@ -1,33 +1,40 @@ { "Provider": "aws", "CheckID": "opensearch_service_domains_use_cognito_authentication_for_kibana", - "CheckTitle": "Check if Amazon Elasticsearch/Opensearch Service domains has either Amazon Cognito or SAML authentication for Kibana enabled", + "CheckTitle": "Amazon OpenSearch Service domain has either Amazon Cognito or SAML authentication enabled for Kibana", "CheckType": [ - "Identify", - "Logging" + "Software and Configuration Checks/AWS Security Best Practices", + "Software and Configuration Checks/Industry and Regulatory Standards/AWS Foundational Security Best Practices", + "TTPs/Initial Access/Unauthorized Access", + "Effects/Data Exposure" ], "ServiceName": "opensearch", "SubServiceName": "", - "ResourceIdTemplate": "arn:partition:service:region:account-id:resource-id", - "Severity": "high", + "ResourceIdTemplate": "", + "Severity": "medium", "ResourceType": "AwsOpenSearchServiceDomain", "ResourceGroup": "database", - "Description": "Check if Amazon Elasticsearch/Opensearch Service domains has Amazon Cognito or SAML authentication for Kibana enabled", - "Risk": "Not enabling Amazon Cognito or SAML authentication for Kibana in AWS Elasticsearch/OpenSearch Service domains increases the likelihood of unauthorized access to sensitive data, potentially compromising system integrity.", + "Description": "**OpenSearch Service domains** use **Amazon Cognito** or **SAML** to authenticate access to Kibana/OpenSearch Dashboards.\n\nThe evaluation identifies domains where either provider is enabled for Dashboards access.", + "Risk": "Without **federated authentication**, Dashboards can be reached using weak or shared credentials or broad IP rules, enabling unauthorized queries and admin actions. This threatens:\n- **Confidentiality**: data exposure\n- **Integrity**: index changes or deletion\n- **Availability**: heavy queries degrading the cluster", "RelatedUrl": "", + "AdditionalURLs": [ + "https://docs.aws.amazon.com/elasticsearch-service/latest/developerguide/es-ac.html" + ], "Remediation": { "Code": { - "CLI": "", - "NativeIaC": "", - "Other": "", - "Terraform": "" + "CLI": "aws opensearch update-domain-config --domain-name --cognito-options Enabled=true,UserPoolId=,IdentityPoolId=,RoleArn=", + "NativeIaC": "```yaml\n# Enable Amazon Cognito authentication for OpenSearch Dashboards\nResources:\n :\n Type: AWS::OpenSearchService::Domain\n Properties:\n DomainName: \n CognitoOptions:\n Enabled: true # Critical: Enables Cognito auth for Dashboards to pass the check\n UserPoolId: \n IdentityPoolId: \n RoleArn: \n```", + "Other": "1. In the AWS console, go to **OpenSearch Service** > **Domains** and select your domain\n2. Click **Edit**\n3. Under **OpenSearch Dashboards authentication**, choose **Amazon Cognito** and enable it\n4. Enter the **User pool ID**, **Identity pool ID**, and **IAM role** for Cognito\n5. Click **Save changes** and wait for the domain update to complete", + "Terraform": "```hcl\n# Enable Amazon Cognito authentication for OpenSearch Dashboards\nresource \"aws_opensearch_domain\" \"\" {\n domain_name = \"\"\n\n cognito_options {\n enabled = true # Critical: Enables Cognito auth for Dashboards to pass the check\n user_pool_id = \"\"\n identity_pool_id = \"\"\n role_arn = \"\"\n }\n}\n```" }, "Recommendation": { - "Text": "If you do not configure Amazon Cognito or SAML authentication, you can still protect Kibana using an IP-based access policy and a proxy server or HTTP basic authentication.", - "Url": "https://docs.aws.amazon.com/elasticsearch-service/latest/developerguide/es-ac.html" + "Text": "Enable **Cognito** or **SAML** for Dashboards and apply **least privilege** with fine-grained access control. Prefer **SSO with MFA**, avoid shared/basic credentials, and restrict access via **VPC/private endpoints** and network controls. Monitor with audit logs and enforce **separation of duties**.", + "Url": "https://hub.prowler.com/check/opensearch_service_domains_use_cognito_authentication_for_kibana" } }, - "Categories": [], + "Categories": [ + "identity-access" + ], "DependsOn": [], "RelatedTo": [], "Notes": "" diff --git a/prowler/providers/aws/services/rds/rds_cluster_backtrack_enabled/rds_cluster_backtrack_enabled.metadata.json b/prowler/providers/aws/services/rds/rds_cluster_backtrack_enabled/rds_cluster_backtrack_enabled.metadata.json index edcc73f639..438b51de95 100644 --- a/prowler/providers/aws/services/rds/rds_cluster_backtrack_enabled/rds_cluster_backtrack_enabled.metadata.json +++ b/prowler/providers/aws/services/rds/rds_cluster_backtrack_enabled/rds_cluster_backtrack_enabled.metadata.json @@ -1,30 +1,40 @@ { "Provider": "aws", "CheckID": "rds_cluster_backtrack_enabled", - "CheckTitle": "Check if RDS Aurora MySQL Clusters have backtrack enabled.", - "CheckType": [], + "CheckTitle": "RDS Aurora MySQL cluster has Backtrack enabled", + "CheckType": [ + "Software and Configuration Checks/AWS Security Best Practices" + ], "ServiceName": "rds", "SubServiceName": "", - "ResourceIdTemplate": "arn:aws:rds:region:account-id:db-cluster", - "Severity": "medium", + "ResourceIdTemplate": "", + "Severity": "low", "ResourceType": "AwsRdsDbCluster", "ResourceGroup": "database", - "Description": "Ensure that the Backtrack feature is enabled for your Amazon Aurora (with MySQL compatibility) database clusters in order to backtrack your clusters to a specific time, without using backups. Backtrack is an Amazon RDS feature that allows you to specify the amount of time that an Aurora MySQL database cluster needs to retain change records, in order to have a fast way to recover from user errors, such as dropping the wrong table or deleting the wrong row by moving your MySQL database to a prior point in time without the need to restore from a recent backup.", - "Risk": "Once the Backtrack feature is enabled, Amazon RDS can quickly 'rewind' your Aurora MySQL database cluster to a point in time that you specify. In contrast to the backup and restore method, with Backtrack you can easily undo a destructive action, such as a DELETE query without a WHERE clause, with minimal downtime, you can rewind your Aurora cluster in just few minutes, and you can repeatedly backtrack a database cluster back and forth in time to help determine when a particular data change occurred.", - "RelatedUrl": "https://docs.aws.amazon.com/securityhub/latest/userguide/rds-controls.html#rds-14", + "Description": "**Aurora MySQL DB clusters** have **Backtrack** configured with a non-zero `BacktrackWindow`, retaining change records to allow rewinding to a consistent earlier time. *Applies to `aurora-mysql` engines only.*", + "Risk": "Without **Backtrack**, destructive queries or admin mistakes can't be quickly undone, forcing snapshot/point-in-time restores. This increases recovery time, disrupts availability, and risks data **integrity** from partial restores or rollbacks.\n\nAdversaries who alter data can cause longer impact windows before containment.", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://docs.aws.amazon.com/config/latest/developerguide/aurora-mysql-backtracking-enabled.html", + "https://docs.aws.amazon.com/securityhub/latest/userguide/rds-controls.html#rds-14", + "https://docs.aws.amazon.com/AmazonRDS/latest/AuroraUserGuide/AuroraMySQL.Managing.Backtrack.html", + "https://www.trendmicro.com/cloudoneconformity/knowledge-base/aws/RDS/backtrack.html#" + ], "Remediation": { "Code": { - "CLI": "aws rds restore-db-cluster-to-point-in-time --region --source-db-cluster-identifier --db-cluster-identifier --restore-type copy-on-write --use-latest-restorable-time --backtrack-window 86400", - "NativeIaC": "https://www.trendmicro.com/cloudoneconformity/knowledge-base/aws/RDS/backtrack.html#", - "Other": "https://www.trendmicro.com/cloudoneconformity/knowledge-base/aws/RDS/backtrack.html#", - "Terraform": "https://www.trendmicro.com/cloudoneconformity/knowledge-base/aws/RDS/backtrack.html#" + "CLI": "aws rds restore-db-cluster-to-point-in-time --source-db-cluster-identifier --db-cluster-identifier --use-latest-restorable-time --backtrack-window 3600", + "NativeIaC": "```yaml\n# CloudFormation: Create Aurora MySQL cluster with Backtrack enabled\nResources:\n :\n Type: AWS::RDS::DBCluster\n Properties:\n Engine: aurora-mysql\n MasterUsername: \"\"\n MasterUserPassword: \"\"\n BacktrackWindow: 3600 # CRITICAL: Enables Backtrack (seconds) so the check passes\n```", + "Other": "1. In the AWS Console, go to RDS > Databases and select the Aurora MySQL cluster\n2. Click Actions > Restore to point in time\n3. Choose Use latest restorable time\n4. Set Backtrack window (seconds) to a value > 0 (e.g., 3600)\n5. Enter a new DB cluster identifier and click Restore DB cluster\n6. Cut over applications to the new cluster", + "Terraform": "```hcl\n# Terraform: Aurora MySQL cluster with Backtrack enabled\nresource \"aws_rds_cluster\" \"\" {\n engine = \"aurora-mysql\"\n master_username = \"\"\n master_password = \"\"\n backtrack_window = 3600 # CRITICAL: Enables Backtrack (seconds) so the check passes\n}\n```" }, "Recommendation": { - "Text": "Backups help you to recover more quickly from a security incident. They also strengthens the resilience of your systems. Aurora backtracking reduces the time to recover a database to a point in time. It does not require a database restore to do so. You cannot enable backtracking on an existing cluster. Instead, you can create a clone that has backtracking enabled.", - "Url": "https://docs.aws.amazon.com/securityhub/latest/userguide/rds-controls.html#rds-14" + "Text": "Enable **Backtrack** on Aurora MySQL clusters and set `BacktrackWindow` to meet RTO while balancing cost and workload. Use it with automated backups for **defense in depth** and resilience.\n\n*For clusters without Backtrack*, provision a clone or new cluster with it enabled; monitor usage and adjust the window as change rates evolve.", + "Url": "https://hub.prowler.com/check/rds_cluster_backtrack_enabled" } }, - "Categories": [], + "Categories": [ + "resilience" + ], "DependsOn": [], "RelatedTo": [], "Notes": "" diff --git a/prowler/providers/aws/services/rds/rds_cluster_copy_tags_to_snapshots/rds_cluster_copy_tags_to_snapshots.metadata.json b/prowler/providers/aws/services/rds/rds_cluster_copy_tags_to_snapshots/rds_cluster_copy_tags_to_snapshots.metadata.json index ad43624e4d..63838f4938 100644 --- a/prowler/providers/aws/services/rds/rds_cluster_copy_tags_to_snapshots/rds_cluster_copy_tags_to_snapshots.metadata.json +++ b/prowler/providers/aws/services/rds/rds_cluster_copy_tags_to_snapshots/rds_cluster_copy_tags_to_snapshots.metadata.json @@ -1,27 +1,33 @@ { "Provider": "aws", "CheckID": "rds_cluster_copy_tags_to_snapshots", - "CheckTitle": "Check if RDS DB clusters have copy tags to snapshots enabled", - "CheckType": [], + "CheckTitle": "RDS DB cluster has copy tags to snapshots enabled", + "CheckType": [ + "Software and Configuration Checks/AWS Security Best Practices" + ], "ServiceName": "rds", "SubServiceName": "", - "ResourceIdTemplate": "arn:aws:rds:region:account-id:db-cluster", + "ResourceIdTemplate": "", "Severity": "low", "ResourceType": "AwsRdsDbCluster", "ResourceGroup": "database", - "Description": "Check if RDS DB clusters have copy tags to snapshots enabled, Aurora instances do not support this feature at instance level so those who are clustered will be scan by this check.", - "Risk": "If RDS clusters are not configured to copy tags to snapshots, it could lead to compliance issues as the snapshots will not inherit necessary metadata such as environment, owner, or purpose tags. This could result in inefficient tracking and management of RDS resources and their snapshots.", - "RelatedUrl": "https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/USER_Tagging.html#USER_Tagging.CopyTags", + "Description": "**RDS DB clusters** are evaluated for the `CopyTagsToSnapshot` setting that propagates cluster tags to their DB snapshots.\n\n*Aurora tagging is configured at the cluster level; instance-level copying isn't supported.*", + "Risk": "**Missing tag propagation** leaves snapshots without consistent metadata, weakening **ABAC**, ownership tracking, and retention controls. This can allow overly broad access to backups, hinder incident scoping, and inflate costs-impacting **confidentiality** and operational governance.", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://docs.aws.amazon.com/securityhub/latest/userguide/rds-controls.html#rds-16", + "https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/USER_Tagging.html#USER_Tagging.CopyTags" + ], "Remediation": { "Code": { - "CLI": "aws rds modify-db-cluster --db-cluster-identifier --copy-tags-to-snapshot", - "NativeIaC": "", - "Other": "https://docs.aws.amazon.com/securityhub/latest/userguide/rds-controls.html#rds-16", - "Terraform": "" + "CLI": "aws rds modify-db-cluster --db-cluster-identifier --copy-tags-to-snapshot", + "NativeIaC": "```yaml\n# CloudFormation: enable copying tags to snapshots on an RDS DB cluster\nResources:\n :\n Type: AWS::RDS::DBCluster\n Properties:\n CopyTagsToSnapshot: true # CRITICAL: ensures cluster tags are copied to snapshots\n```", + "Other": "1. In the AWS Console, go to RDS > Databases\n2. Select the DB cluster and choose Modify\n3. Check Copy tags to snapshots\n4. Click Continue, then Apply changes", + "Terraform": "```hcl\n# Terraform: enable copying tags to snapshots on an RDS DB cluster\nresource \"aws_rds_cluster\" \"\" {\n copy_tags_to_snapshot = true # CRITICAL: ensures cluster tags are copied to snapshots\n}\n```" }, "Recommendation": { - "Text": "Ensure that the `CopyTagsToSnapshot` setting is enabled for all RDS clusters to propagate cluster tags to their snapshots for improved tracking and compliance.", - "Url": "https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/USER_Tagging.html#USER_Tagging.CopyTags" + "Text": "Enable `CopyTagsToSnapshot` on all applicable **RDS/Aurora clusters**.\n- Standardize required tags (owner, environment, data class)\n- Use **least privilege** and **ABAC** based on tags\n- Automate tagging and periodic audits so snapshots inherit metadata and lifecycle policies", + "Url": "https://hub.prowler.com/check/rds_cluster_copy_tags_to_snapshots" } }, "Categories": [], diff --git a/prowler/providers/aws/services/rds/rds_cluster_critical_event_subscription/rds_cluster_critical_event_subscription.metadata.json b/prowler/providers/aws/services/rds/rds_cluster_critical_event_subscription/rds_cluster_critical_event_subscription.metadata.json index d41b20eab0..f0b819df1a 100644 --- a/prowler/providers/aws/services/rds/rds_cluster_critical_event_subscription/rds_cluster_critical_event_subscription.metadata.json +++ b/prowler/providers/aws/services/rds/rds_cluster_critical_event_subscription/rds_cluster_critical_event_subscription.metadata.json @@ -1,32 +1,39 @@ { "Provider": "aws", "CheckID": "rds_cluster_critical_event_subscription", - "CheckTitle": "Check if RDS Cluster critical events are subscribed.", + "CheckTitle": "RDS cluster event subscription is enabled for maintenance and failure categories", "CheckType": [ - "Software and Configuration Checks, AWS Security Best Practices" + "Software and Configuration Checks/AWS Security Best Practices" ], "ServiceName": "rds", "SubServiceName": "", - "ResourceIdTemplate": "arn:aws:rds:region:account-id:account", - "Severity": "low", - "ResourceType": "AwsAccount", - "ResourceGroup": "governance", - "Description": "Ensure that Amazon RDS event notification subscriptions are enabled for database cluster events, particularly maintenance and failure.", - "Risk": "Without event subscriptions for critical events, such as maintenance and failures, you may not be aware of issues affecting your RDS clusters, leading to downtime or security vulnerabilities.", - "RelatedUrl": "https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/USER_Events.html", + "ResourceIdTemplate": "", + "Severity": "medium", + "ResourceType": "AwsRdsEventSubscription", + "ResourceGroup": "database", + "Description": "**RDS event subscriptions** for the `db-cluster` source type are enabled and cover critical cluster event categories: **`maintenance`** and **`failure`** (or all cluster events).", + "Risk": "Without notifications for **cluster maintenance** and **failure**, teams may miss engine updates, failovers, or node crashes, impacting **availability** and increasing MTTR. Prolonged degraded states can cause replication lag and potential **data integrity** issues, hindering timely containment.", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/USER_Events.html", + "https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/USER_Events.Subscribing.html", + "https://docs.aws.amazon.com/securityhub/latest/userguide/rds-controls.html#rds-19" + ], "Remediation": { "Code": { - "CLI": "aws rds create-event-subscription --source-type db-cluster --event-categories 'failure' 'maintenance' --sns-topic-arn ", - "NativeIaC": "", - "Other": "https://docs.aws.amazon.com/securityhub/latest/userguide/rds-controls.html#rds-19", - "Terraform": "" + "CLI": "aws rds create-event-subscription --subscription-name --sns-topic-arn --source-type db-cluster --event-categories maintenance failure --enabled", + "NativeIaC": "```yaml\nResources:\n :\n Type: AWS::RDS::EventSubscription\n Properties:\n SnsTopicArn: # Critical: SNS topic to receive notifications\n SourceType: db-cluster # Critical: Scope to DB clusters\n EventCategories: # Critical: Subscribe to required categories only\n - maintenance\n - failure\n Enabled: true # Critical: Must be enabled to pass\n```", + "Other": "1. In the AWS Console, go to RDS > Event subscriptions > Create event subscription\n2. Name: enter \n3. Send notifications to: Choose existing Amazon SNS ARN and select \n4. Source type: select Clusters\n5. Event categories: select only Maintenance and Failure (unselect others)\n6. Ensure Enabled is on\n7. Click Create", + "Terraform": "```hcl\nresource \"aws_db_event_subscription\" \"\" {\n name = \"\"\n sns_topic = \"\" # Critical: SNS topic ARN\n source_type = \"db-cluster\" # Critical: Scope to clusters\n event_categories = [\"maintenance\", \"failure\"] # Critical: Required categories\n enabled = true # Critical: Must be enabled\n}\n```" }, "Recommendation": { - "Text": "To subscribe to RDS cluster event notifications, see Subscribing to Amazon RDS event notification in the Amazon RDS User Guide.", - "Url": "https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/USER_Events.Subscribing.html" + "Text": "Enable **event subscriptions** for RDS clusters that include `maintenance` and `failure`, delivered via **SNS** to monitored channels.\n- Enforce **least privilege** on topics\n- Separate topics per environment\n- Integrate with on-call/IR playbooks and test alerts\n- Add multiple recipients and escalation for **defense in depth**", + "Url": "https://hub.prowler.com/check/rds_cluster_critical_event_subscription" } }, - "Categories": [], + "Categories": [ + "logging" + ], "DependsOn": [], "RelatedTo": [], "Notes": "" diff --git a/prowler/providers/aws/services/rds/rds_cluster_default_admin/rds_cluster_default_admin.metadata.json b/prowler/providers/aws/services/rds/rds_cluster_default_admin/rds_cluster_default_admin.metadata.json index c01afd054a..b3a7075636 100644 --- a/prowler/providers/aws/services/rds/rds_cluster_default_admin/rds_cluster_default_admin.metadata.json +++ b/prowler/providers/aws/services/rds/rds_cluster_default_admin/rds_cluster_default_admin.metadata.json @@ -1,30 +1,40 @@ { "Provider": "aws", "CheckID": "rds_cluster_default_admin", - "CheckTitle": "Ensure that your Amazon RDS clusters are not using the default master username.", - "CheckType": [], + "CheckTitle": "RDS cluster master username is not admin or postgres", + "CheckType": [ + "Software and Configuration Checks/AWS Security Best Practices", + "TTPs/Initial Access", + "TTPs/Credential Access" + ], "ServiceName": "rds", "SubServiceName": "", - "ResourceIdTemplate": "arn:aws:rds:region:account-id:db-cluster", + "ResourceIdTemplate": "", "Severity": "medium", "ResourceType": "AwsRdsDbCluster", "ResourceGroup": "database", - "Description": "Ensure that your Amazon RDS clusters are not using the default master username.", - "Risk": "Since admin is the Amazon's example for the RDS database master username and postgres is the default PostgreSQL master username. Many AWS customers will use this username for their RDS database instances in production. Malicious users can use this information to their advantage and frequently try to use default master username during brute-force attacks.", - "RelatedUrl": "https://docs.aws.amazon.com/securityhub/latest/userguide/rds-controls.html#rds-24", + "Description": "RDS DB clusters are evaluated for use of a **custom administrator username**, flagging clusters that use defaults such as `admin` or `postgres`.", + "Risk": "Using a well-known admin username simplifies **credential stuffing** and **brute-force** attempts. Attackers can focus on password guessing, increasing chances of compromise. A successful login enables data theft (**confidentiality**), unauthorized changes (**integrity**), and destructive operations or outages (**availability**).", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://www.trendmicro.com/cloudoneconformity/knowledge-base/aws/RDS/rds-master-username.html#", + "https://docs.aws.amazon.com/securityhub/latest/userguide/rds-controls.html#rds-24" + ], "Remediation": { "Code": { "CLI": "", - "NativeIaC": "https://www.trendmicro.com/cloudoneconformity/knowledge-base/aws/RDS/rds-master-username.html#", - "Other": "https://www.trendmicro.com/cloudoneconformity/knowledge-base/aws/RDS/rds-master-username.html#", - "Terraform": "https://www.trendmicro.com/cloudoneconformity/knowledge-base/aws/RDS/rds-master-username.html#" + "NativeIaC": "```yaml\n# Create an RDS DB cluster with a custom admin username\nResources:\n :\n Type: AWS::RDS::DBCluster\n Properties:\n Engine: aurora-mysql\n MasterUsername: # CRITICAL: use a non-default username (not \"admin\" or \"postgres\")\n MasterUserPassword: \n```", + "Other": "1. In the AWS console, go to RDS > Databases > Create database\n2. Choose Amazon Aurora and the same engine family as your current cluster\n3. Under Settings, set Master username to a custom value that is not \"admin\" or \"postgres\" (CRITICAL)\n4. Create the new cluster, migrate your workload to it, update application connections, then delete the old cluster", + "Terraform": "```hcl\n# Create an RDS DB cluster with a custom admin username\nresource \"aws_rds_cluster\" \"\" {\n cluster_identifier = \"\"\n engine = \"aurora-mysql\"\n master_username = \"\" # CRITICAL: non-default username (not admin/postgres)\n master_password = \"\"\n}\n```" }, "Recommendation": { - "Text": "To change the master username configured for your Amazon RDS database clusters you must re-create them and migrate the existing data.", - "Url": "https://docs.aws.amazon.com/securityhub/latest/userguide/rds-controls.html#rds-24" + "Text": "Create databases with a **unique, non-default admin username** that doesn't reveal environment or org. Apply **least privilege** by using separate, non-admin accounts for applications. Prefer **IAM database authentication** and manage secrets centrally with rotation. Restrict admin access and monitor login attempts.", + "Url": "https://hub.prowler.com/check/rds_cluster_default_admin" } }, - "Categories": [], + "Categories": [ + "identity-access" + ], "DependsOn": [], "RelatedTo": [], "Notes": "" diff --git a/prowler/providers/aws/services/rds/rds_cluster_deletion_protection/rds_cluster_deletion_protection.metadata.json b/prowler/providers/aws/services/rds/rds_cluster_deletion_protection/rds_cluster_deletion_protection.metadata.json index c5516f84a5..b82b1cdd75 100644 --- a/prowler/providers/aws/services/rds/rds_cluster_deletion_protection/rds_cluster_deletion_protection.metadata.json +++ b/prowler/providers/aws/services/rds/rds_cluster_deletion_protection/rds_cluster_deletion_protection.metadata.json @@ -1,30 +1,40 @@ { "Provider": "aws", "CheckID": "rds_cluster_deletion_protection", - "CheckTitle": "Check if RDS clusters have deletion protection enabled.", - "CheckType": [], + "CheckTitle": "RDS cluster has deletion protection enabled", + "CheckType": [ + "Software and Configuration Checks/AWS Security Best Practices", + "Software and Configuration Checks/Industry and Regulatory Standards/AWS Foundational Security Best Practices", + "Effects/Data Destruction" + ], "ServiceName": "rds", "SubServiceName": "", - "ResourceIdTemplate": "arn:aws:rds:region:account-id:db-cluster", - "Severity": "low", + "ResourceIdTemplate": "", + "Severity": "medium", "ResourceType": "AwsRdsDbCluster", "ResourceGroup": "database", - "Description": "Check if RDS clusters have deletion protection enabled.", - "Risk": "You can only delete clusters that do not have deletion protection enabled.", - "RelatedUrl": "https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/USER_DeleteInstance.html", + "Description": "**RDS DB clusters** have **deletion protection** enabled (`deletion_protection=true`).", + "Risk": "Without **deletion protection**, a cluster can be removed by error or misuse, causing sudden **availability** loss and potential **data loss** if backups are outdated. Compromised identities or faulty automation can trigger destructive deletes, degrading **recoverability** and data **integrity**.", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/USER_DeleteInstance.html", + "https://docs.aws.amazon.com/securityhub/latest/userguide/rds-controls.html#rds-7" + ], "Remediation": { "Code": { - "CLI": "aws rds modify-db-cluster --db-cluster-identifier --deletion-protection --apply-immediately", - "NativeIaC": "", - "Other": "https://docs.aws.amazon.com/securityhub/latest/userguide/rds-controls.html#rds-7", - "Terraform": "https://docs.prowler.com/checks/aws/general-policies/ensure-that-rds-clusters-and-instances-have-deletion-protection-enabled#terraform" + "CLI": "aws rds modify-db-cluster --db-cluster-identifier --deletion-protection", + "NativeIaC": "```yaml\n# CloudFormation: Enable deletion protection on an RDS DB Cluster\nResources:\n :\n Type: AWS::RDS::DBCluster\n Properties:\n Engine: aurora-mysql\n DeletionProtection: true # Critical: prevents cluster deletion and passes the check\n```", + "Other": "1. In the AWS Console, go to RDS > Databases\n2. Select the DB cluster (type: DB cluster)\n3. Click Modify\n4. Enable Deletion protection\n5. Choose Apply immediately and click Modify cluster", + "Terraform": "```hcl\n# Enable deletion protection on an existing RDS DB Cluster\nresource \"aws_rds_cluster\" \"\" {\n deletion_protection = true # Critical: prevents cluster deletion and passes the check\n}\n```" }, "Recommendation": { - "Text": "Enable deletion protection using the AWS Management Console for production DB clusters.", - "Url": "https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/USER_DeleteInstance.html" + "Text": "Enable **deletion protection** (`deletion_protection=true`) on production and other critical clusters. Enforce via IaC and organizational guardrails; apply **least privilege** to delete/modify actions; require **change control** and approvals. Maintain reliable **backups** to restore when protection must be lifted.", + "Url": "https://hub.prowler.com/check/rds_cluster_deletion_protection" } }, - "Categories": [], + "Categories": [ + "resilience" + ], "DependsOn": [], "RelatedTo": [], "Notes": "" diff --git a/prowler/providers/aws/services/rds/rds_cluster_iam_authentication_enabled/rds_cluster_iam_authentication_enabled.metadata.json b/prowler/providers/aws/services/rds/rds_cluster_iam_authentication_enabled/rds_cluster_iam_authentication_enabled.metadata.json index 163e831e38..090d62173a 100644 --- a/prowler/providers/aws/services/rds/rds_cluster_iam_authentication_enabled/rds_cluster_iam_authentication_enabled.metadata.json +++ b/prowler/providers/aws/services/rds/rds_cluster_iam_authentication_enabled/rds_cluster_iam_authentication_enabled.metadata.json @@ -1,30 +1,42 @@ { "Provider": "aws", "CheckID": "rds_cluster_iam_authentication_enabled", - "CheckTitle": "Check if RDS clusters have IAM authentication enabled.", - "CheckType": [], + "CheckTitle": "RDS cluster has IAM authentication enabled", + "CheckType": [ + "Software and Configuration Checks/AWS Security Best Practices", + "Software and Configuration Checks/Industry and Regulatory Standards/AWS Foundational Security Best Practices", + "TTPs/Credential Access" + ], "ServiceName": "rds", "SubServiceName": "", - "ResourceIdTemplate": "arn:aws:rds:region:account-id:db-cluster", + "ResourceIdTemplate": "", "Severity": "medium", "ResourceType": "AwsRdsDbCluster", "ResourceGroup": "database", - "Description": "Check if RDS clusters have IAM authentication enabled.", - "Risk": "Ensure that the IAM Database Authentication feature is enabled for your RDS database clusters in order to use the Identity and Access Management (IAM) service to manage database access to your MySQL and PostgreSQL database clusters. With this feature enabled, you don't have to use a password when you connect to your MySQL/PostgreSQL database, instead you can use an authentication token. An authentication token is a unique string of characters with a lifetime of 15 minutes that Amazon RDS generates on your request. IAM Database Authentication removes the need of storing user credentials within the database configuration, because authentication is managed externally using Amazon IAM.", - "RelatedUrl": "https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/UsingWithRDS.IAMDBAuth.Enabling.html", + "Description": "**RDS DB clusters** on supported engines (MySQL/MariaDB/PostgreSQL/Aurora) have **IAM database authentication** enabled for database logins, indicating token-based access managed by IAM instead of static passwords.", + "Risk": "Without **IAM DB authentication**, databases depend on long-lived passwords. Stolen or reused creds can enable unauthorized connections, data exfiltration, and tampering, harming **confidentiality** and **integrity**. Lacking short-lived tokens and centralized revocation expands blast radius and weakens **access control**.", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/UsingWithRDS.IAMDBAuth.Enabling.html", + "https://www.trendmicro.com/cloudoneconformity/knowledge-base/aws/RDS/iam-database-authentication.html#", + "https://docs.aws.amazon.com/AmazonRDS/latest/AuroraUserGuide/UsingWithRDS.IAMDBAuth.Enabling.html", + "https://docs.aws.amazon.com/securityhub/latest/userguide/rds-controls.html#rds-12" + ], "Remediation": { "Code": { - "CLI": "aws rds modify-db-instance --region --db-instance-identifier --enable-iam-database-authentication --apply-immediately", - "NativeIaC": "https://www.trendmicro.com/cloudoneconformity/knowledge-base/aws/RDS/iam-database-authentication.html#", - "Other": "https://docs.aws.amazon.com/securityhub/latest/userguide/rds-controls.html#rds-12", - "Terraform": "https://www.trendmicro.com/cloudoneconformity/knowledge-base/aws/RDS/iam-database-authentication.html#" + "CLI": "aws rds modify-db-cluster --db-cluster-identifier --enable-iam-database-authentication --apply-immediately", + "NativeIaC": "```yaml\n# CloudFormation: Enable IAM authentication on an existing RDS/Aurora DB Cluster\nResources:\n :\n Type: AWS::RDS::DBCluster\n Properties:\n EnableIAMDatabaseAuthentication: true # Critical: turns on IAM DB auth for the cluster\n```", + "Other": "1. In the AWS Console, go to RDS > Databases\n2. Select the DB Cluster (not the instances)\n3. Click Modify\n4. In Database authentication, enable IAM database authentication\n5. Choose Apply immediately and click Modify cluster", + "Terraform": "```hcl\n# Enable IAM authentication on an existing RDS/Aurora cluster\nresource \"aws_rds_cluster\" \"\" {\n cluster_identifier = \"\"\n iam_database_authentication_enabled = true # Critical: enables IAM DB auth so the check passes\n}\n```" }, "Recommendation": { - "Text": "Enable IAM authentication for supported RDS clusters.", - "Url": "https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/UsingWithRDS.IAMDBAuth.Enabling.html" + "Text": "Enable **IAM database authentication** on supported clusters and enforce **least privilege**. Grant only necessary `rds-db:connect` permissions to specific principals, prefer role-based access for workloads to obtain short-lived tokens, require **TLS**, and deprecate static DB passwords. Pair with auditing and segmentation for **defense in depth**.", + "Url": "https://hub.prowler.com/check/rds_cluster_iam_authentication_enabled" } }, - "Categories": [], + "Categories": [ + "identity-access" + ], "DependsOn": [], "RelatedTo": [], "Notes": "" diff --git a/prowler/providers/aws/services/rds/rds_cluster_integration_cloudwatch_logs/rds_cluster_integration_cloudwatch_logs.metadata.json b/prowler/providers/aws/services/rds/rds_cluster_integration_cloudwatch_logs/rds_cluster_integration_cloudwatch_logs.metadata.json index 803989cb30..c6acca9336 100644 --- a/prowler/providers/aws/services/rds/rds_cluster_integration_cloudwatch_logs/rds_cluster_integration_cloudwatch_logs.metadata.json +++ b/prowler/providers/aws/services/rds/rds_cluster_integration_cloudwatch_logs/rds_cluster_integration_cloudwatch_logs.metadata.json @@ -1,27 +1,35 @@ { "Provider": "aws", "CheckID": "rds_cluster_integration_cloudwatch_logs", - "CheckTitle": "Check if RDS cluster is integrated with CloudWatch Logs.", - "CheckType": [], + "CheckTitle": "RDS cluster has CloudWatch Logs export enabled", + "CheckType": [ + "Software and Configuration Checks/AWS Security Best Practices/Runtime Behavior Analysis", + "Software and Configuration Checks/Industry and Regulatory Standards/AWS Foundational Security Best Practices" + ], "ServiceName": "rds", "SubServiceName": "", - "ResourceIdTemplate": "arn:aws:rds:region:account-id:db-cluster", + "ResourceIdTemplate": "", "Severity": "medium", "ResourceType": "AwsRdsDbCluster", "ResourceGroup": "database", - "Description": "Check if RDS cluster is integrated with CloudWatch Logs. The types valid are Aurora MySQL, Aurora PostgreSQL, MySQL, PostgreSQL.", - "Risk": "If logs are not enabled, monitoring of service use and threat analysis is not possible.", - "RelatedUrl": "https://docs.aws.amazon.com/AmazonRDS/latest/AuroraUserGuide/USER_LogAccess.html", + "Description": "**RDS clusters** running Aurora MySQL, Aurora PostgreSQL, MySQL, or PostgreSQL are assessed for **CloudWatch Logs publishing**, confirming that database logs are exported to a CloudWatch Logs group.", + "Risk": "Without publishing to CloudWatch, database events lack centralized visibility and retention.\nBrute-force, SQL injection, or privilege misuse may go undetected, hindering forensics and response, risking data confidentiality and integrity and delaying recovery.", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/publishing_cloudwatchlogs.html", + "https://docs.aws.amazon.com/AmazonRDS/latest/AuroraUserGuide/USER_LogAccess.html", + "https://docs.aws.amazon.com/securityhub/latest/userguide/rds-controls.html#rds-34" + ], "Remediation": { "Code": { - "CLI": "aws rds modify-db-cluster --db-cluster-identifier --cloudwatch-logs-export-configuration {'EnableLogTypes':['audit',error','general','slowquery']} --apply-immediately", - "NativeIaC": "", - "Other": "https://docs.aws.amazon.com/securityhub/latest/userguide/rds-controls.html#rds-34", - "Terraform": "" + "CLI": "aws rds modify-db-cluster --db-cluster-identifier --cloudwatch-logs-export-configuration '{\"EnableLogTypes\":[\"\"]}' --apply-immediately", + "NativeIaC": "```yaml\n# CloudFormation: Enable CloudWatch Logs export for an RDS/Aurora DB cluster\nResources:\n :\n Type: AWS::RDS::DBCluster\n Properties:\n Engine: \n MasterUsername: \n MasterUserPassword: \n EnableCloudwatchLogsExports:\n - # CRITICAL: Enables at least one supported log type (e.g., 'error' for MySQL or 'postgresql' for PostgreSQL) to pass the check\n```", + "Other": "1. In the AWS Console, go to RDS > Databases\n2. Select your DB cluster and choose Modify\n3. Under Log exports, check at least one supported log type for your engine (e.g., error/general/slowquery/audit for MySQL, postgresql for PostgreSQL)\n4. Choose Continue, then Apply immediately, and click Modify cluster", + "Terraform": "```hcl\n# Terraform: Enable CloudWatch Logs export for an RDS/Aurora DB cluster\nresource \"aws_rds_cluster\" \"\" {\n engine = \"\"\n master_username = \"\"\n master_password = \"\"\n\n enabled_cloudwatch_logs_exports = [\n \"\" # CRITICAL: Enables at least one supported log type (e.g., \"error\" for MySQL or \"postgresql\" for PostgreSQL) to pass the check\n ]\n}\n```" }, "Recommendation": { - "Text": "Use CloudWatch Logs to perform real-time analysis of the log data. Create alarms and view metrics.", - "Url": "https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/publishing_cloudwatchlogs.html" + "Text": "Publish RDS/Aurora logs to **CloudWatch Logs** and centralize analysis.\nSelect appropriate types (e.g., `error`, `general`, `slowquery`, `audit`), define retention, and create alarms. Limit log access with **least privilege** and integrate with SIEM for defense-in-depth monitoring.", + "Url": "https://hub.prowler.com/check/rds_cluster_integration_cloudwatch_logs" } }, "Categories": [ diff --git a/prowler/providers/aws/services/rds/rds_cluster_minor_version_upgrade_enabled/rds_cluster_minor_version_upgrade_enabled.metadata.json b/prowler/providers/aws/services/rds/rds_cluster_minor_version_upgrade_enabled/rds_cluster_minor_version_upgrade_enabled.metadata.json index 08882bf0b9..87df083f42 100644 --- a/prowler/providers/aws/services/rds/rds_cluster_minor_version_upgrade_enabled/rds_cluster_minor_version_upgrade_enabled.metadata.json +++ b/prowler/providers/aws/services/rds/rds_cluster_minor_version_upgrade_enabled/rds_cluster_minor_version_upgrade_enabled.metadata.json @@ -1,30 +1,41 @@ { "Provider": "aws", "CheckID": "rds_cluster_minor_version_upgrade_enabled", - "CheckTitle": "Ensure RDS clusters have minor version upgrade enabled.", - "CheckType": [], + "CheckTitle": "RDS cluster has automatic minor version upgrades enabled", + "CheckType": [ + "Software and Configuration Checks/Patch Management", + "Software and Configuration Checks/AWS Security Best Practices", + "Software and Configuration Checks/Industry and Regulatory Standards/AWS Foundational Security Best Practices" + ], "ServiceName": "rds", "SubServiceName": "", - "ResourceIdTemplate": "arn:aws:rds:region:account-id:db-cluster", + "ResourceIdTemplate": "", "Severity": "medium", "ResourceType": "AwsRdsDbCluster", "ResourceGroup": "database", - "Description": "Ensure RDS clusters have minor version upgrade enabled.", - "Risk": "Auto Minor Version Upgrade is a feature that you can enable to have your database automatically upgraded when a new minor database engine version is available. Minor version upgrades often patch security vulnerabilities and fix bugs and therefore should be applied.", - "RelatedUrl": "https://aws.amazon.com/blogs/database/best-practices-for-upgrading-amazon-rds-to-major-and-minor-versions-of-postgresql/", + "Description": "**RDS Multi-AZ DB clusters** are configured for **automatic minor engine upgrades** via `auto_minor_version_upgrade`.\n\nThe evaluation checks these clusters to see if this setting is enabled so preferred minor releases are applied during the maintenance window.", + "Risk": "Without automatic minor upgrades, clusters can miss **security patches**, enabling exploitation of known flaws (confidentiality). Unfixed defects may corrupt data (**integrity**) or trigger crashes and failovers (**availability**). Emergency patching raises downtime and operational risk.", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/modify-multi-az-db-cluster.html", + "https://docs.aws.amazon.com/sdk-for-ruby/v3/api/Aws/RDS/Types/CreateDBClusterMessage.html", + "https://docs.aws.amazon.com/securityhub/latest/userguide/rds-controls.html#rds-35" + ], "Remediation": { "Code": { - "CLI": "aws rds modify-db-cluster --db-cluster-identifier --auto-minor-version-upgrade --apply-immediately", - "NativeIaC": "https://docs.prowler.com/checks/aws/general-policies/ensure-aws-db-instance-gets-all-minor-upgrades-automatically#cloudformation", - "Other": "https://docs.aws.amazon.com/securityhub/latest/userguide/rds-controls.html#rds-35", - "Terraform": "https://docs.prowler.com/checks/aws/general-policies/ensure-aws-db-instance-gets-all-minor-upgrades-automatically#terraform" + "CLI": "aws rds modify-db-cluster --db-cluster-identifier --auto-minor-version-upgrade", + "NativeIaC": "```yaml\n# CloudFormation snippet to enable automatic minor version upgrades on an RDS DB cluster\nResources:\n :\n Type: AWS::RDS::DBCluster\n Properties:\n AutoMinorVersionUpgrade: true # Critical: enables automatic minor engine version upgrades to pass the check\n```", + "Other": "1. In the AWS Console, go to RDS > Databases\n2. Select your Multi-AZ DB cluster\n3. Click Modify\n4. Set Auto minor version upgrade to Enabled\n5. Click Continue, then Modify cluster", + "Terraform": "```hcl\n# Terraform snippet to enable automatic minor version upgrades on an RDS DB cluster\nresource \"aws_rds_cluster\" \"\" {\n cluster_identifier = \"\"\n auto_minor_version_upgrade = true # Critical: enables automatic minor engine version upgrades to pass the check\n}\n```" }, "Recommendation": { - "Text": "Enable auto minor version upgrade for all databases and environments.", - "Url": "https://aws.amazon.com/blogs/database/best-practices-for-upgrading-amazon-rds-to-major-and-minor-versions-of-postgresql/" + "Text": "Enable `auto_minor_version_upgrade` on **RDS Multi-AZ clusters** and align updates with approved maintenance windows. Validate changes in non-production, and document any exceptions with a strict manual patch cadence. This strengthens **defense in depth** and improves **availability**.", + "Url": "https://hub.prowler.com/check/rds_cluster_minor_version_upgrade_enabled" } }, - "Categories": [], + "Categories": [ + "vulnerabilities" + ], "DependsOn": [], "RelatedTo": [], "Notes": "" diff --git a/prowler/providers/aws/services/rds/rds_cluster_multi_az/rds_cluster_multi_az.metadata.json b/prowler/providers/aws/services/rds/rds_cluster_multi_az/rds_cluster_multi_az.metadata.json index 4a2c41c0de..78a4e0c0cd 100644 --- a/prowler/providers/aws/services/rds/rds_cluster_multi_az/rds_cluster_multi_az.metadata.json +++ b/prowler/providers/aws/services/rds/rds_cluster_multi_az/rds_cluster_multi_az.metadata.json @@ -1,31 +1,39 @@ { "Provider": "aws", "CheckID": "rds_cluster_multi_az", - "CheckTitle": "Check if RDS clusters have multi-AZ enabled.", - "CheckType": [], + "CheckTitle": "RDS cluster has Multi-AZ enabled", + "CheckType": [ + "Software and Configuration Checks/AWS Security Best Practices", + "Industry and Regulatory Standards/AWS Foundational Security Best Practices" + ], "ServiceName": "rds", "SubServiceName": "", - "ResourceIdTemplate": "arn:aws:rds:region:account-id:db-cluster", + "ResourceIdTemplate": "", "Severity": "medium", "ResourceType": "AwsRdsDbCluster", "ResourceGroup": "database", - "Description": "Check if RDS clusters have multi-AZ enabled.", - "Risk": "In case of failure, with a single-AZ deployment configuration, should an availability zone specific database failure occur, Amazon RDS can not automatically fail over to the standby availability zone.", - "RelatedUrl": "https://aws.amazon.com/rds/features/multi-az/", + "Description": "**RDS DB clusters** are assessed for deployment across **multiple Availability Zones** (*Multi-AZ*), verifying that redundant instances exist to support **automatic failover** instead of a single-AZ configuration.", + "Risk": "A **single-AZ cluster** is a single point of failure. AZ outages, instance/storage faults, or maintenance can cause extended **downtime**, failed transactions, and worse **RTO/RPO**. Without cross-AZ replicas, recovery may require restores, increasing outage duration and risking service disruption and data consistency impacts.", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://docs.aws.amazon.com/securityhub/latest/userguide/rds-controls.html#rds-15", + "https://docs.amazonaws.cn/en_us/AmazonRDS/latest/UserGuide/Concepts.MultiAZ.html", + "https://aws.amazon.com/rds/features/multi-az/" + ], "Remediation": { "Code": { - "CLI": "aws rds create-db-cluster --db-cluster-identifier --multi-az true", - "NativeIaC": "https://docs.prowler.com/checks/aws/general-policies/general_73#cloudformation", - "Other": "https://docs.aws.amazon.com/securityhub/latest/userguide/rds-controls.html#rds-15", - "Terraform": "https://docs.prowler.com/checks/aws/general-policies/general_73#terraform" + "CLI": "", + "NativeIaC": "```yaml\n# CloudFormation: create an RDS Multi-AZ DB cluster (non-Aurora)\nResources:\n :\n Type: AWS::RDS::DBCluster\n Properties:\n Engine: mysql # CRITICAL: using mysql/postgres with the properties below creates a Multi-AZ DB cluster\n DBClusterInstanceClass: db.r6g.large # CRITICAL: required to make it a Multi-AZ DB cluster (creates 1 writer + 2 readers across AZs)\n AllocatedStorage: 100 # CRITICAL: required for Multi-AZ DB clusters\n Iops: 1000 # CRITICAL: required for Multi-AZ DB clusters\n StorageType: io1 # CRITICAL: required for Multi-AZ DB clusters\n MasterUsername: \n MasterUserPassword: \n```", + "Other": "1. In the AWS console, go to RDS > Databases > Create database\n2. Engine: select MySQL or PostgreSQL\n3. Under Availability and durability, select Multi-AZ DB cluster\n4. Enter DB cluster identifier and master credentials\n5. Choose a DB instance class and create the database\n6. Migrate data to this new Multi-AZ DB cluster and switch your applications to its endpoint", + "Terraform": "```hcl\n# Terraform: create an RDS Multi-AZ DB cluster (non-Aurora)\nresource \"aws_rds_cluster\" \"\" {\n cluster_identifier = \"\"\n engine = \"mysql\" # CRITICAL: mysql/postgres with the lines below creates a Multi-AZ DB cluster\n db_cluster_instance_class = \"db.r6g.large\" # CRITICAL: makes this a Multi-AZ DB cluster (1 writer + 2 readers across AZs)\n storage_type = \"io1\" # CRITICAL: required for Multi-AZ DB clusters\n allocated_storage = 100 # CRITICAL: required for Multi-AZ DB clusters\n iops = 1000 # CRITICAL: required for Multi-AZ DB clusters\n master_username = \"\"\n master_password = \"\"\n}\n```" }, "Recommendation": { - "Text": "Enable multi-AZ deployment for production databases.", - "Url": "https://aws.amazon.com/rds/features/multi-az/" + "Text": "Enable **Multi-AZ** for production DB clusters to ensure cross-AZ redundancy and **automatic failover**. Choose a model that meets your SLA (one standby or two readable standbys; Aurora spans 3 AZs). Place subnets in distinct AZs, implement connection retries, and regularly test failover to validate **RTO/RPO** and readiness.", + "Url": "https://hub.prowler.com/check/rds_cluster_multi_az" } }, "Categories": [ - "redundancy" + "resilience" ], "DependsOn": [], "RelatedTo": [], diff --git a/prowler/providers/aws/services/rds/rds_cluster_non_default_port/rds_cluster_non_default_port.metadata.json b/prowler/providers/aws/services/rds/rds_cluster_non_default_port/rds_cluster_non_default_port.metadata.json index 2eea7fd1c9..824cde1e6f 100644 --- a/prowler/providers/aws/services/rds/rds_cluster_non_default_port/rds_cluster_non_default_port.metadata.json +++ b/prowler/providers/aws/services/rds/rds_cluster_non_default_port/rds_cluster_non_default_port.metadata.json @@ -1,29 +1,34 @@ { "Provider": "aws", "CheckID": "rds_cluster_non_default_port", - "CheckTitle": "Check if RDS clusters are using non-default ports.", + "CheckTitle": "RDS cluster uses a non-default port for its database engine", "CheckType": [ - "Software and Configuration Checks/AWS Security Best Practices" + "Software and Configuration Checks/AWS Security Best Practices/Network Reachability", + "TTPs/Discovery" ], "ServiceName": "rds", "SubServiceName": "", - "ResourceIdTemplate": "arn:aws:rds:region:account-id:cluster:db-cluster", + "ResourceIdTemplate": "", "Severity": "low", "ResourceType": "AwsRdsDbCluster", "ResourceGroup": "database", - "Description": "Checks if an cluster uses a port other than the default port of the database engine. The control fails if the RDS cluster uses the default port.", - "Risk": "Using a default database port exposes the cluster to potential security vulnerabilities, as attackers are more likely to target known, commonly-used ports. This may result in unauthorized access to the database or increased susceptibility to automated attacks.", - "RelatedUrl": "https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/Overview.DBInstance.Modifying.html", + "Description": "**RDS DB clusters** are assessed for use of a **non-default database port**.\n\nEvaluation focuses on whether the cluster listens on the engine's well-known default port (e.g., `3306`, `5432`, `1433`, `1521`, `50000`) or on a custom port.", + "Risk": "Using a **default DB port** eases discovery via mass scans and banner-grabbing, enabling:\n- Engine-specific exploit attempts\n- Brute-force/credential-stuffing of logins\n- Targeted DDoS on the service\n\nImpacts: **confidentiality** (unauthorized reads), **integrity** (data changes), **availability** (service disruption).", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/Overview.DBInstance.Modifying.html", + "https://docs.aws.amazon.com/securityhub/latest/userguide/rds-controls.html#rds-23" + ], "Remediation": { "Code": { - "CLI": "aws rds modify-db-cluster --db-cluster-identifier --port ", - "NativeIaC": "", - "Other": "https://docs.aws.amazon.com/securityhub/latest/userguide/rds-controls.html#rds-23", - "Terraform": "" + "CLI": "aws rds modify-db-cluster --db-cluster-identifier --port --apply-immediately", + "NativeIaC": "```yaml\n# CloudFormation: Set a non-default port on an RDS/Aurora DB cluster\nResources:\n :\n Type: AWS::RDS::DBCluster\n Properties:\n Engine: \n MasterUsername: \n MasterUserPassword: \n Port: # Critical: change to a non-default engine port to pass the check\n```", + "Other": "1. In the AWS Console, go to RDS > Databases\n2. Select your DB cluster (not an individual instance)\n3. Click Modify\n4. Set Database port to a non-default value for the engine\n5. Check Apply immediately\n6. Click Continue, then Modify cluster", + "Terraform": "```hcl\n# Terraform: Set a non-default port on an RDS/Aurora DB cluster\nresource \"aws_rds_cluster\" \"\" {\n engine = \"\"\n master_username = \"\"\n master_password = \"\"\n port = # Critical: use a non-default engine port to pass the check\n}\n```" }, "Recommendation": { - "Text": "Modify the RDS cluster to use a non-default port, and ensure that the security group permits access to the new port.", - "Url": "https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/Overview.DBInstance.Modifying.html" + "Text": "Use a **non-default port** and enforce **least-privilege** network access:\n- Allow only approved sources\n- Keep databases in private subnets\n- Require TLS and strong, centralized auth\n- Monitor failed connections\n\nUpdate application connection strings to the new `port` as part of defense-in-depth.", + "Url": "https://hub.prowler.com/check/rds_cluster_non_default_port" } }, "Categories": [], diff --git a/prowler/providers/aws/services/rds/rds_cluster_protected_by_backup_plan/rds_cluster_protected_by_backup_plan.metadata.json b/prowler/providers/aws/services/rds/rds_cluster_protected_by_backup_plan/rds_cluster_protected_by_backup_plan.metadata.json index 39b4a84294..46429ed196 100644 --- a/prowler/providers/aws/services/rds/rds_cluster_protected_by_backup_plan/rds_cluster_protected_by_backup_plan.metadata.json +++ b/prowler/providers/aws/services/rds/rds_cluster_protected_by_backup_plan/rds_cluster_protected_by_backup_plan.metadata.json @@ -1,32 +1,39 @@ { "Provider": "aws", "CheckID": "rds_cluster_protected_by_backup_plan", - "CheckTitle": "Check if RDS clusters are protected by a backup plan.", + "CheckTitle": "RDS cluster is protected by an AWS Backup plan", "CheckType": [ - "Software and Configuration Checks, AWS Security Best Practices" + "Software and Configuration Checks/AWS Security Best Practices", + "Software and Configuration Checks/Industry and Regulatory Standards/AWS Foundational Security Best Practices" ], "ServiceName": "rds", "SubServiceName": "", - "ResourceIdTemplate": "arn:aws:rds:region:account-id:db-cluster", - "Severity": "medium", - "ResourceType": "AwsRdsDbInstance", + "ResourceIdTemplate": "", + "Severity": "high", + "ResourceType": "AwsRdsDbCluster", "ResourceGroup": "database", - "Description": "Check if RDS clusters are protected by a backup plan.", - "Risk": "Without a backup plan, RDS clusters are vulnerable to data loss, accidental deletion, or corruption. This could lead to significant operational disruptions or loss of critical data.", - "RelatedUrl": "https://docs.aws.amazon.com/aws-backup/latest/devguide/assigning-resources.html", + "Description": "**RDS DB clusters** are covered by an **AWS Backup backup plan** when resource assignments include the cluster, either explicitly, by tags, or via an appropriate resource scope.", + "Risk": "Lack of centralized backups enables irreversible **data loss** and **corruption**, reducing **availability** and **integrity**. Accidental deletion, ransomware, or bad changes may become unrecoverable. Weak retention or no immutability undermines rollback and can breach recovery objectives.", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://docs.aws.amazon.com/aws-backup/latest/devguide/assigning-resources.html", + "https://docs.aws.amazon.com/securityhub/latest/userguide/rds-controls.html#rds-26" + ], "Remediation": { "Code": { - "CLI": "aws backup create-backup-plan --backup-plan , aws backup tag-resource --resource-arn --tags Key=backup,Value=true", - "NativeIaC": "", - "Other": "https://docs.aws.amazon.com/securityhub/latest/userguide/rds-controls.html#rds-26", - "Terraform": "" + "CLI": "aws backup create-backup-selection --backup-plan-id --backup-selection '{\"SelectionName\":\"rds-clusters\",\"IamRoleArn\":\"arn:aws:iam:::role/service-role/AWSBackupDefaultServiceRole\",\"Resources\":[\"arn:aws:rds:*:*:cluster:*\"]}'", + "NativeIaC": "```yaml\n# CloudFormation: assign RDS clusters to an AWS Backup plan\nResources:\n :\n Type: AWS::Backup::BackupSelection\n Properties:\n BackupPlanId: \"\"\n BackupSelection:\n SelectionName: \"rds-clusters\"\n IamRoleArn: \"arn:aws:iam:::role/service-role/AWSBackupDefaultServiceRole\"\n Resources:\n - \"arn:aws:rds:*:*:cluster:*\" # Critical: includes all RDS clusters in the plan to mark them protected\n```", + "Other": "1. In the AWS Backup console, go to Settings > Service opt-in and enable RDS if it is not enabled.\n2. Go to Backup plans and select an existing plan (create a minimal plan if none exists).\n3. Click Assign resources.\n4. Set a name and choose IAM role: AWSBackupDefaultServiceRole.\n5. Under Resources, choose By resource ID and select the target RDS DB cluster (or use the ARN), then click Assign resources.\n6. The cluster now appears as protected by the backup plan.", + "Terraform": "```hcl\n# Assign RDS clusters to an existing AWS Backup plan\nresource \"aws_backup_selection\" \"\" {\n name = \"rds-clusters\"\n plan_id = \"\"\n iam_role_arn = \"arn:aws:iam:::role/service-role/AWSBackupDefaultServiceRole\"\n\n resources = [\n \"arn:aws:rds:*:*:cluster:*\" # Critical: includes all RDS clusters in the plan to pass the check\n ]\n}\n```" }, "Recommendation": { - "Text": "Create a backup plan for the RDS cluster to protect it from data loss, accidental deletion, or corruption.", - "Url": "https://docs.aws.amazon.com/aws-backup/latest/devguide/assigning-resources.html" + "Text": "Include RDS clusters in an **AWS Backup backup plan**. Apply **defense in depth**: define schedules and retention, enable immutable vault controls and cross-Region copies, use tags for consistent coverage, enforce **least privilege** for backup roles, and regularly test restores to validate RPO/RTO.", + "Url": "https://hub.prowler.com/check/rds_cluster_protected_by_backup_plan" } }, - "Categories": [], + "Categories": [ + "resilience" + ], "DependsOn": [], "RelatedTo": [], "Notes": "" diff --git a/prowler/providers/aws/services/rds/rds_cluster_storage_encrypted/rds_cluster_storage_encrypted.metadata.json b/prowler/providers/aws/services/rds/rds_cluster_storage_encrypted/rds_cluster_storage_encrypted.metadata.json index 9575463b3c..67267e6a47 100644 --- a/prowler/providers/aws/services/rds/rds_cluster_storage_encrypted/rds_cluster_storage_encrypted.metadata.json +++ b/prowler/providers/aws/services/rds/rds_cluster_storage_encrypted/rds_cluster_storage_encrypted.metadata.json @@ -1,30 +1,40 @@ { "Provider": "aws", "CheckID": "rds_cluster_storage_encrypted", - "CheckTitle": "Check if RDS clusters storage is encrypted.", - "CheckType": [], + "CheckTitle": "RDS cluster storage is encrypted", + "CheckType": [ + "Software and Configuration Checks/AWS Security Best Practices", + "Software and Configuration Checks/Industry and Regulatory Standards/AWS Foundational Security Best Practices" + ], "ServiceName": "rds", "SubServiceName": "", - "ResourceIdTemplate": "arn:aws:rds:region:account-id:db-cluster", - "Severity": "medium", + "ResourceIdTemplate": "", + "Severity": "high", "ResourceType": "AwsRdsDbCluster", "ResourceGroup": "database", - "Description": "Check if RDS clusters storage is encrypted.", - "Risk": "If not enabled sensitive information at rest is not protected.", - "RelatedUrl": "https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/Overview.Encryption.html", + "Description": "**RDS DB clusters** are assessed for **encryption at rest** via AWS KMS. It determines whether cluster storage-and related artifacts like automated backups and snapshots-are encrypted with a KMS key.", + "Risk": "Unencrypted clusters expose data on disk, logs, and snapshots to unauthorized reading if storage or backups are obtained. This compromises **confidentiality**, enables offline exfiltration from leaked snapshots, and can widen blast radius during incidents and migrations.", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://docs.aws.amazon.com/securityhub/latest/userguide/rds-controls.html#rds-27", + "https://docs.aws.amazon.com/AmazonRDS/latest/AuroraUserGuide/Overview.Encryption.html#Overview.Encryption.Enabling", + "https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/Overview.Encryption.html" + ], "Remediation": { "Code": { - "CLI": "aws rds create-db-cluster --db-cluster-identifier --db-cluster-class --engine --storage-encrypted true", - "NativeIaC": "", - "Other": "https://docs.aws.amazon.com/securityhub/latest/userguide/rds-controls.html#rds-27", - "Terraform": "" + "CLI": "", + "NativeIaC": "```yaml\n# CloudFormation: Create an encrypted RDS/Aurora DB cluster\nResources:\n :\n Type: AWS::RDS::DBCluster\n Properties:\n Engine: \n MasterUsername: \n MasterUserPassword: \n StorageEncrypted: true # Critical: enables encryption at rest for the cluster\n```", + "Other": "1. In the AWS Console, go to RDS > Databases > Create database\n2. Select your engine (Aurora or Multi-AZ DB cluster)\n3. In the configuration, enable Storage encryption (Enable encryption)\n4. Leave the KMS key as default (aws/rds or aws/aurora) unless you require a custom key\n5. Create the cluster, migrate traffic, then delete the unencrypted cluster", + "Terraform": "```hcl\n# Terraform: Create an encrypted RDS/Aurora DB cluster\nresource \"aws_rds_cluster\" \"\" {\n engine = \"\"\n master_username = \"\"\n master_password = \"\"\n storage_encrypted = true # Critical: enables encryption at rest for the cluster\n}\n```" }, "Recommendation": { - "Text": "Enable Encryption. Use a CMK where possible. It will provide additional management and privacy benefits.", - "Url": "https://docs.aws.amazon.com/AmazonRDS/latest/AuroraUserGuide/Overview.Encryption.html#Overview.Encryption.Enabling" + "Text": "Create clusters with `StorageEncrypted=true` using **AWS KMS**, preferably **customer-managed keys**. Apply **least privilege** to key usage, enable rotation and monitoring, and separate key administration from DB operations. Ensure snapshots and cross-account copies remain encrypted for **defense in depth**.", + "Url": "https://hub.prowler.com/check/rds_cluster_storage_encrypted" } }, - "Categories": [], + "Categories": [ + "encryption" + ], "DependsOn": [], "RelatedTo": [], "Notes": "" diff --git a/prowler/providers/aws/services/rds/rds_instance_backup_enabled/rds_instance_backup_enabled.metadata.json b/prowler/providers/aws/services/rds/rds_instance_backup_enabled/rds_instance_backup_enabled.metadata.json index 50da9652bb..5c10c56b9c 100644 --- a/prowler/providers/aws/services/rds/rds_instance_backup_enabled/rds_instance_backup_enabled.metadata.json +++ b/prowler/providers/aws/services/rds/rds_instance_backup_enabled/rds_instance_backup_enabled.metadata.json @@ -1,30 +1,40 @@ { "Provider": "aws", "CheckID": "rds_instance_backup_enabled", - "CheckTitle": "Check if RDS instances have backup enabled.", - "CheckType": [], + "CheckTitle": "RDS instance has backup retention period greater than 0 days", + "CheckType": [ + "Software and Configuration Checks/AWS Security Best Practices", + "Software and Configuration Checks/Industry and Regulatory Standards/AWS Foundational Security Best Practices", + "Effects/Data Destruction" + ], "ServiceName": "rds", "SubServiceName": "", - "ResourceIdTemplate": "arn:aws:rds:region:account-id:db-instance", + "ResourceIdTemplate": "", "Severity": "medium", "ResourceType": "AwsRdsDbInstance", "ResourceGroup": "database", - "Description": "Check if RDS instances have backup enabled.", - "Risk": "If backup is not enabled, data is vulnerable. Human error or bad actors could erase or modify data.", - "RelatedUrl": "https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/USER_WorkingWithAutomatedBackups.html", + "Description": "**RDS DB instances** are evaluated for **automated backups** by confirming the backup retention period is greater than `0` days, indicating point-in-time recovery is configured.", + "Risk": "Without automated backups, you lose **point-in-time recovery**, impacting **availability** and **integrity**.\n\nAccidental deletes, destructive queries, or compromised accounts can cause unrecoverable data loss and prolonged outages, preventing reliable rollback during incidents.", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/USER_WorkingWithAutomatedBackups.html", + "https://www.trendmicro.com/cloudoneconformity/knowledge-base/aws/RDS/rds-automated-backups-enabled.html" + ], "Remediation": { "Code": { - "CLI": "aws rds modify-db-instance --db-instance-identifier --backup-retention-period 7 --apply-immediately", - "NativeIaC": "", - "Other": "https://www.trendmicro.com/cloudoneconformity/knowledge-base/aws/RDS/rds-automated-backups-enabled.html", - "Terraform": "https://docs.prowler.com/checks/aws/general-policies/ensure-that-rds-instances-have-backup-policy#terraform" + "CLI": "aws rds modify-db-instance --db-instance-identifier --backup-retention-period 1 --apply-immediately", + "NativeIaC": "```yaml\n# CloudFormation: enable automated backups on an RDS instance\nResources:\n :\n Type: AWS::RDS::DBInstance\n Properties:\n DBInstanceClass: db.t3.micro\n Engine: mysql\n AllocatedStorage: 20\n MasterUsername: admin\n MasterUserPassword: \n BackupRetentionPeriod: 1 # CRITICAL: Enables automated backups (>0 days)\n```", + "Other": "1. Open the AWS Management Console and go to RDS > Databases\n2. Select the target DB instance and click Modify\n3. In Backup section, set Backup retention period to 1 day (or more)\n4. Check Apply immediately\n5. Click Continue (if shown) and then Modify DB instance", + "Terraform": "```hcl\n# Terraform: enable automated backups on an RDS instance\nresource \"aws_db_instance\" \"\" {\n allocated_storage = 20\n engine = \"mysql\"\n instance_class = \"db.t3.micro\"\n username = \"admin\"\n password = \"\"\n backup_retention_period = 1 # CRITICAL: Enables automated backups (>0 days)\n}\n```" }, "Recommendation": { - "Text": "Enable automated backup for production data. Define a retention period and periodically test backup restoration. A Disaster Recovery process should be in place to govern Data Protection approach.", - "Url": "https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/USER_WorkingWithAutomatedBackups.html" + "Text": "Enable **automated backups** with retention > `0` aligned to RPO/RTO. Regularly test restores to validate **PITR**.\n\nApply **least privilege** to backup access, encrypt snapshots, and replicate critical backups to separate locations for **defense in depth** and resilient recovery.", + "Url": "https://hub.prowler.com/check/rds_instance_backup_enabled" } }, - "Categories": [], + "Categories": [ + "resilience" + ], "DependsOn": [], "RelatedTo": [], "Notes": "" diff --git a/prowler/providers/aws/services/rds/rds_instance_certificate_expiration/rds_instance_certificate_expiration.metadata.json b/prowler/providers/aws/services/rds/rds_instance_certificate_expiration/rds_instance_certificate_expiration.metadata.json index 490008f1b7..b50ad339dd 100644 --- a/prowler/providers/aws/services/rds/rds_instance_certificate_expiration/rds_instance_certificate_expiration.metadata.json +++ b/prowler/providers/aws/services/rds/rds_instance_certificate_expiration/rds_instance_certificate_expiration.metadata.json @@ -1,27 +1,35 @@ { "Provider": "aws", "CheckID": "rds_instance_certificate_expiration", - "CheckTitle": "Ensure that the SSL/TLS certificates configured for your Amazon RDS are not expired.", - "CheckType": [], + "CheckTitle": "RDS instance SSL/TLS certificate has more than 3 months of validity remaining", + "CheckType": [ + "Software and Configuration Checks/AWS Security Best Practices", + "Software and Configuration Checks/Patch Management", + "Software and Configuration Checks/Industry and Regulatory Standards/AWS Foundational Security Best Practices" + ], "ServiceName": "rds", "SubServiceName": "", - "ResourceIdTemplate": "arn:aws:rds:region:account-id:db-instance", + "ResourceIdTemplate": "", "Severity": "high", "ResourceType": "AwsRdsDbInstance", "ResourceGroup": "database", - "Description": "To maintain Amazon RDS database security and avoid interruption of the applications that are using RDS and/or Aurora databases, rotate the required SSL/TLS certificates and update the deprecated Certificate Authority (CA) certificates at the Amazon RDS instance level.", - "Risk": "Interruption of application if the certificate expires.", - "RelatedUrl": "https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/UsingWithRDS.SSL-certificate-rotation.html", + "Description": "**RDS DB instances** are evaluated for **server certificate validity** windows, including default and **customer-managed certificates**. Certificates **expired** or **approaching expiration** (e.g., `<1 month`, `<3 months`, `3-6 months`, `>6 months`) are identified using the certificate `valid_till` date.", + "Risk": "Without timely rotation:\n- TLS failures block DB connections, impacting **availability**\n- Bypassed or weak validation enables **MITM**, risking **confidentiality** and **integrity**\n- Emergency changes increase **operational risk** and error rates", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/UsingWithRDS.SSL-certificate-rotation.html", + "https://www.trendmicro.com/cloudoneconformity/knowledge-base/aws/RDS/rotate-rds-certificates.html" + ], "Remediation": { "Code": { - "CLI": "aws rds modify-db-instance --region us-east-1 --db-instance-identifier cc-project5-mysql-database --ca-certificate-identifier \"rds-ca-2019\" --apply-immediately", - "NativeIaC": "", - "Other": "https://www.trendmicro.com/cloudoneconformity/knowledge-base/aws/RDS/rotate-rds-certificates.html", - "Terraform": "" + "CLI": "aws rds modify-db-instance --db-instance-identifier --ca-certificate-identifier rds-ca-rsa2048-g1 --apply-immediately", + "NativeIaC": "```yaml\n# CloudFormation: update RDS instance CA to a current certificate\nResources:\n :\n Type: AWS::RDS::DBInstance\n Properties:\n CACertificateIdentifier: rds-ca-rsa2048-g1 # CRITICAL: rotates to a valid CA to restore >3 months certificate validity\n```", + "Other": "1. In the AWS Console, go to RDS > Databases and select the DB instance\n2. Click Modify\n3. In Connectivity (or Certificate authority), select rds-ca-rsa2048-g1\n4. Check Apply immediately\n5. Click Continue (if shown) and then Modify DB instance", + "Terraform": "```hcl\n# Set a current CA on the RDS instance\nresource \"aws_db_instance\" \"\" {\n identifier = \"\"\n ca_cert_identifier = \"rds-ca-rsa2048-g1\" # CRITICAL: rotates to a valid CA to ensure certificate validity >3 months\n}\n```" }, "Recommendation": { - "Text": "To maintain Amazon RDS database security and avoid interruption of the applications that are using RDS and/or Aurora databases, rotate the required SSL/TLS certificates and update the deprecated Certificate Authority (CA) certificates at the Amazon RDS instance level.", - "Url": "https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/UsingWithRDS.SSL-certificate-rotation.html" + "Text": "Establish a **certificate lifecycle** for RDS:\n- Rotate server/CA certs well before expiry; avoid pinned or outdated CAs\n- Keep client trust stores current and enforce TLS with validation\n- Monitor expiry and automate alerts/rotation\n- For custom certs, apply **least privilege**, **separation of duties**, and periodic key rotation; test changes", + "Url": "https://hub.prowler.com/check/rds_instance_certificate_expiration" } }, "Categories": [ diff --git a/prowler/providers/aws/services/rds/rds_instance_copy_tags_to_snapshots/rds_instance_copy_tags_to_snapshots.metadata.json b/prowler/providers/aws/services/rds/rds_instance_copy_tags_to_snapshots/rds_instance_copy_tags_to_snapshots.metadata.json index 5711cbdff2..4258b0d101 100644 --- a/prowler/providers/aws/services/rds/rds_instance_copy_tags_to_snapshots/rds_instance_copy_tags_to_snapshots.metadata.json +++ b/prowler/providers/aws/services/rds/rds_instance_copy_tags_to_snapshots/rds_instance_copy_tags_to_snapshots.metadata.json @@ -1,27 +1,34 @@ { "Provider": "aws", "CheckID": "rds_instance_copy_tags_to_snapshots", - "CheckTitle": "Check if RDS DB instances have copy tags to snapshots enabled", - "CheckType": [], + "CheckTitle": "RDS DB instance has copy tags to snapshots enabled", + "CheckType": [ + "Software and Configuration Checks/AWS Security Best Practices" + ], "ServiceName": "rds", "SubServiceName": "", - "ResourceIdTemplate": "arn:aws:rds:region:account-id:db-instance", + "ResourceIdTemplate": "", "Severity": "low", "ResourceType": "AwsRdsDbInstance", "ResourceGroup": "database", - "Description": "Check if RDS DB instances have copy tags to snapshots enabled, Aurora instances can not have this feature enabled at this level, they will have it at cluster level", - "Risk": "If RDS instances are not configured to copy tags to snapshots, it could lead to compliance issues as the snapshots will not inherit necessary metadata such as environment, owner, or purpose tags. This could result in inefficient tracking and management of RDS resources and their snapshots.", - "RelatedUrl": "https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/USER_Tagging.html#USER_Tagging.CopyTags", + "Description": "**RDS DB instances** are assessed for propagating instance tags to their **DB snapshots** using `CopyTagsToSnapshot`.\n\n*Aurora engines manage this at the cluster level and aren't evaluated per instance.*", + "Risk": "Snapshots without inherited tags lose ownership, environment, and sensitivity context, degrading visibility and governance. Missing metadata weakens **ABAC**, cost allocation, and lifecycle policies, enabling unintended backup access, orphaned snapshots, and retention drift that impact confidentiality and availability.", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://docs.aws.amazon.com/systems-manager-automation-runbooks/latest/userguide/automation-aws-enable-tags-snapshot-rds-instance.html", + "https://docs.aws.amazon.com/securityhub/latest/userguide/rds-controls.html#rds-17", + "https://www.trendmicro.com/cloudoneconformity/knowledge-base/aws/RDS/copy-tags-to-snapshot.html" + ], "Remediation": { "Code": { - "CLI": "aws rds modify-db-instance --db-instance-identifier --copy-tags-to-snapshot", - "NativeIaC": "", - "Other": "https://docs.aws.amazon.com/securityhub/latest/userguide/rds-controls.html#rds-17", - "Terraform": "" + "CLI": "aws rds modify-db-instance --db-instance-identifier --copy-tags-to-snapshot --apply-immediately", + "NativeIaC": "```yaml\n# CloudFormation: enable copying tags to snapshots on an RDS DB instance\nResources:\n :\n Type: AWS::RDS::DBInstance\n Properties:\n CopyTagsToSnapshot: true # Critical: ensures DB instance tags are copied to snapshots\n```", + "Other": "1. In the AWS Console, go to RDS > Databases and select the non-Aurora DB instance\n2. Click Modify\n3. Under Additional configuration, enable Copy tags to snapshots\n4. Check Apply immediately and click Modify DB instance", + "Terraform": "```hcl\n# Terraform: enable copying tags to snapshots on an RDS DB instance\nresource \"aws_db_instance\" \"\" {\n copy_tags_to_snapshot = true # Critical: ensures DB instance tags are copied to snapshots\n}\n```" }, "Recommendation": { - "Text": "Ensure that the `CopyTagsToSnapshot` setting is enabled for all RDS instances to propagate instance tags to their snapshots for improved tracking and compliance.", - "Url": "https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/Overview.DBInstance.Modifying.html" + "Text": "Enable `CopyTagsToSnapshot` on non-Aurora RDS instances so snapshots inherit required metadata. Establish a consistent **tag taxonomy** and automate enforcement to support **least privilege** via ABAC, cost tracking, and retention controls. For Aurora, configure tag copy at the cluster level.", + "Url": "https://hub.prowler.com/check/rds_instance_copy_tags_to_snapshots" } }, "Categories": [], diff --git a/prowler/providers/aws/services/rds/rds_instance_critical_event_subscription/rds_instance_critical_event_subscription.metadata.json b/prowler/providers/aws/services/rds/rds_instance_critical_event_subscription/rds_instance_critical_event_subscription.metadata.json index e26e7b984a..97f17a6a96 100644 --- a/prowler/providers/aws/services/rds/rds_instance_critical_event_subscription/rds_instance_critical_event_subscription.metadata.json +++ b/prowler/providers/aws/services/rds/rds_instance_critical_event_subscription/rds_instance_critical_event_subscription.metadata.json @@ -1,32 +1,40 @@ { "Provider": "aws", "CheckID": "rds_instance_critical_event_subscription", - "CheckTitle": "Check if RDS Instances events are subscribed.", + "CheckTitle": "RDS instance event subscription is enabled for maintenance, configuration change, and failure categories", "CheckType": [ - "Software and Configuration Checks, AWS Security Best Practices" + "Software and Configuration Checks/AWS Security Best Practices" ], "ServiceName": "rds", "SubServiceName": "", - "ResourceIdTemplate": "arn:aws:rds:region:account-id:db-instance", - "Severity": "low", + "ResourceIdTemplate": "", + "Severity": "medium", "ResourceType": "AwsRdsEventSubscription", "ResourceGroup": "database", - "Description": "Ensure that Amazon RDS event notification subscriptions are enabled for database database events, particularly maintenance, configuration change and failure.", - "Risk": "Without event subscriptions for critical events, such as maintenance, configuration changes and failures, you may not be aware of issues affecting your RDS instances, leading to downtime or security vulnerabilities.", - "RelatedUrl": "https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/USER_Events.html", + "Description": "**RDS event subscriptions** for DB instances are assessed for coverage of the critical categories `maintenance`, `configuration change`, and `failure`.\n\nThe evaluation looks for enabled `db-instance` subscriptions and confirms these categories are included or that all events are selected.", + "Risk": "Without these notifications, critical DB events go unseen, undermining **availability** and **integrity**. Missed outages, unexpected restarts, or silent parameter changes can delay response, prolong downtime, corrupt data flows, and leave misconfigurations unaddressed.", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/USER_Events.Subscribing.html", + "https://docs.aws.amazon.com/securityhub/latest/userguide/rds-controls.html", + "https://www.trendmicro.com/cloudoneconformity/knowledge-base/aws/RDS/rds-instance-level-events.html", + "https://docs.aws.amazon.com/securityhub/latest/userguide/rds-controls.html#rds-20" + ], "Remediation": { "Code": { - "CLI": "aws rds create-event-subscription --source-type db-instance --event-categories 'failure' 'maintenance' 'configuration change' --sns-topic-arn ", - "NativeIaC": "", - "Other": "https://docs.aws.amazon.com/securityhub/latest/userguide/rds-controls.html#rds-20", - "Terraform": "" + "CLI": "aws rds create-event-subscription --subscription-name --sns-topic-arn --source-type db-instance --event-categories \"maintenance\" \"configuration change\" \"failure\"", + "NativeIaC": "```yaml\n# CloudFormation: RDS DB instance event subscription\nResources:\n :\n Type: AWS::RDS::EventSubscription\n Properties:\n SnsTopicArn: # critical: SNS topic to receive notifications\n SourceType: db-instance # critical: subscribe to DB instance events\n EventCategories: # critical: required categories for PASS\n - maintenance\n - configuration change\n - failure\n```", + "Other": "1. Open the AWS Console and go to RDS\n2. In the left menu, select Event subscriptions > Create event subscription\n3. Send notifications to: select an existing SNS topic ()\n4. Source type: choose Instances\n5. Event categories: select Maintenance, Configuration change, and Failure\n6. Create the subscription", + "Terraform": "```hcl\n# Terraform: RDS DB instance event subscription\nresource \"aws_db_event_subscription\" \"\" {\n name = \"\"\n sns_topic = \"\"\n source_type = \"db-instance\" # critical: DB instance events\n event_categories = [\"maintenance\", \"configuration change\", \"failure\"] # critical: required categories\n}\n```" }, "Recommendation": { - "Text": "To subscribe to RDS instance event notifications, see Subscribing to Amazon RDS event notification in the Amazon RDS User Guide.", - "Url": "https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/USER_Events.Subscribing.html" + "Text": "Establish and sustain **RDS event subscriptions** for `db-instance` that include `maintenance`, `configuration change`, and `failure`.\n- Deliver to monitored channels (ticketing/chat/paging)\n- Enforce **least privilege** on topics\n- Test alert delivery and runbooks\n- Periodically review coverage across Regions", + "Url": "https://hub.prowler.com/check/rds_instance_critical_event_subscription" } }, - "Categories": [], + "Categories": [ + "logging" + ], "DependsOn": [], "RelatedTo": [], "Notes": "" diff --git a/prowler/providers/aws/services/rds/rds_instance_default_admin/rds_instance_default_admin.metadata.json b/prowler/providers/aws/services/rds/rds_instance_default_admin/rds_instance_default_admin.metadata.json index 2f4ae743c4..ce8b7b036b 100644 --- a/prowler/providers/aws/services/rds/rds_instance_default_admin/rds_instance_default_admin.metadata.json +++ b/prowler/providers/aws/services/rds/rds_instance_default_admin/rds_instance_default_admin.metadata.json @@ -1,30 +1,39 @@ { "Provider": "aws", "CheckID": "rds_instance_default_admin", - "CheckTitle": "Ensure that your Amazon RDS instances are not using the default master username.", - "CheckType": [], + "CheckTitle": "RDS instance does not use the default master username (admin or postgres)", + "CheckType": [ + "Software and Configuration Checks/AWS Security Best Practices", + "TTPs/Credential Access" + ], "ServiceName": "rds", "SubServiceName": "", - "ResourceIdTemplate": "arn:aws:rds:region:account-id:db-instance", + "ResourceIdTemplate": "", "Severity": "medium", "ResourceType": "AwsRdsDbInstance", "ResourceGroup": "database", - "Description": "Ensure that your Amazon RDS instances are not using the default master username.", - "Risk": "Since admin is the Amazon's example for the RDS database master username and postgres is the default PostgreSQL master username. Many AWS customers will use this username for their RDS database instances in production. Malicious users can use this information to their advantage and frequently try to use default master username during brute-force attacks.", - "RelatedUrl": "https://docs.aws.amazon.com/securityhub/latest/userguide/rds-controls.html#rds-25", + "Description": "**RDS DB instances** are evaluated for use of a **custom administrator username**. The finding identifies instances or clusters where the admin user matches common defaults like `admin` or `postgres` (checked at the instance or cluster level).", + "Risk": "Using a predictable admin name enables **password spraying**, **credential stuffing**, and username **enumeration**. A successful login can expose data (**confidentiality**), allow tampering (**integrity**), and disrupt service (**availability**). Defaults also attract automated scans targeting known usernames.", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://docs.aws.amazon.com/securityhub/latest/userguide/rds-controls.html#rds-25", + "https://www.trendmicro.com/cloudoneconformity/knowledge-base/aws/RDS/rds-master-username.html#" + ], "Remediation": { "Code": { "CLI": "", - "NativeIaC": "https://www.trendmicro.com/cloudoneconformity/knowledge-base/aws/RDS/rds-master-username.html#", - "Other": "https://www.trendmicro.com/cloudoneconformity/knowledge-base/aws/RDS/rds-master-username.html#", - "Terraform": "https://www.trendmicro.com/cloudoneconformity/knowledge-base/aws/RDS/rds-master-username.html#" + "NativeIaC": "```yaml\n# CloudFormation: Create an RDS instance with a non-default master username\nResources:\n :\n Type: AWS::RDS::DBInstance\n Properties:\n Engine: mysql\n DBInstanceClass: db.t3.micro\n AllocatedStorage: 20\n MasterUsername: # Critical: use a custom admin username (not \"admin\" or \"postgres\")\n MasterUserPassword: \n```", + "Other": "1. In the AWS Console, go to RDS > Databases and click Create database\n2. Choose your engine and select Standard create\n3. In Settings, set Master username to a value that is not \"admin\" or \"postgres\"\n4. Complete creation and note the new endpoint\n5. Migrate data from the old instance to the new one (e.g., dump/restore or replication)\n6. Update applications to use the new endpoint, then delete the old instance\n7. If the instance is part of an Aurora cluster, create a new cluster with a non-default master username and migrate to it", + "Terraform": "```hcl\n# Terraform: Create an RDS instance with a non-default master username\nresource \"aws_db_instance\" \"\" {\n engine = \"mysql\"\n instance_class = \"db.t3.micro\"\n allocated_storage = 20\n username = \"\" # Critical: custom admin username (not \"admin\" or \"postgres\")\n password = \"\"\n}\n```" }, "Recommendation": { - "Text": "To change the master username configured for your Amazon RDS database instances you must re-create them and migrate the existing data.", - "Url": "https://docs.aws.amazon.com/securityhub/latest/userguide/rds-controls.html#rds-25" + "Text": "Adopt a **unique, non-default admin username** for each database and avoid enabling default accounts.\n- Enforce **least privilege** with separate admin and app users\n- Use strong, rotated secrets in a manager and prefer **IAM DB authentication**\n- Restrict network exposure and audit authentication activity", + "Url": "https://hub.prowler.com/check/rds_instance_default_admin" } }, - "Categories": [], + "Categories": [ + "identity-access" + ], "DependsOn": [], "RelatedTo": [], "Notes": "" diff --git a/prowler/providers/aws/services/rds/rds_instance_deletion_protection/rds_instance_deletion_protection.metadata.json b/prowler/providers/aws/services/rds/rds_instance_deletion_protection/rds_instance_deletion_protection.metadata.json index a1643359b6..7c406bef48 100644 --- a/prowler/providers/aws/services/rds/rds_instance_deletion_protection/rds_instance_deletion_protection.metadata.json +++ b/prowler/providers/aws/services/rds/rds_instance_deletion_protection/rds_instance_deletion_protection.metadata.json @@ -1,30 +1,40 @@ { "Provider": "aws", "CheckID": "rds_instance_deletion_protection", - "CheckTitle": "Check if RDS instances have deletion protection enabled.", - "CheckType": [], + "CheckTitle": "RDS instance has deletion protection enabled", + "CheckType": [ + "Software and Configuration Checks/AWS Security Best Practices", + "Software and Configuration Checks/Industry and Regulatory Standards/AWS Foundational Security Best Practices", + "Effects/Data Destruction" + ], "ServiceName": "rds", "SubServiceName": "", - "ResourceIdTemplate": "arn:aws:rds:region:account-id:db-instance", + "ResourceIdTemplate": "", "Severity": "medium", "ResourceType": "AwsRdsDbInstance", "ResourceGroup": "database", - "Description": "Check if RDS instances have deletion protection enabled.", - "Risk": "You can only delete instances that do not have deletion protection enabled.", - "RelatedUrl": "https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/USER_DeleteInstance.html", + "Description": "**RDS DB instances** are assessed for **deletion protection**. If an instance belongs to an Aurora cluster, the setting is evaluated at the cluster level; otherwise, it is evaluated on the instance itself.", + "Risk": "Without **deletion protection**, a user or pipeline can delete a database in one action, causing immediate loss of availability and possible data loss if backups are stale or missing. This heightens exposure to insider misuse, compromised credentials, or faulty automation, increasing recovery time and cost.", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://www.trendmicro.com/cloudoneconformity/knowledge-base/aws/RDS/instance-deletion-protection.html", + "https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/USER_DeleteInstance.html" + ], "Remediation": { "Code": { "CLI": "aws rds modify-db-instance --db-instance-identifier --deletion-protection --apply-immediately", - "NativeIaC": "", - "Other": "https://www.trendmicro.com/cloudoneconformity/knowledge-base/aws/RDS/instance-deletion-protection.html", - "Terraform": "https://docs.prowler.com/checks/aws/general-policies/ensure-that-rds-clusters-and-instances-have-deletion-protection-enabled#terraform" + "NativeIaC": "```yaml\n# CloudFormation: enable deletion protection\nResources:\n :\n Type: AWS::RDS::DBInstance\n Properties:\n DeletionProtection: true # Critical: enables deletion protection for standalone instances\n\n :\n Type: AWS::RDS::DBCluster\n Properties:\n DeletionProtection: true # Critical: enables deletion protection at cluster level (required for Aurora members)\n```", + "Other": "1. In the AWS console, go to RDS > Databases\n2. For a standalone DB instance: select the instance > Modify > enable Deletion protection > Continue > Apply immediately > Modify DB instance\n3. For an Aurora/clustered instance: switch to the cluster (Writer) > Modify > enable Deletion protection > Continue > Apply immediately > Modify cluster", + "Terraform": "```hcl\n# Enable deletion protection on a standalone RDS instance\nresource \"aws_db_instance\" \"\" {\n deletion_protection = true # Critical: prevents instance deletion\n}\n\n# Enable deletion protection on an RDS/Aurora cluster\nresource \"aws_rds_cluster\" \"\" {\n deletion_protection = true # Critical: prevents cluster deletion (required for instances in this cluster)\n}\n```" }, "Recommendation": { - "Text": "Enable deletion protection using the AWS Management Console for production DB instances.", - "Url": "https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/USER_DeleteInstance.html" + "Text": "Enable `deletion protection` on production RDS instances and Aurora clusters. Enforce **least privilege** for delete/modify actions and require change control to disable protection. Use **defense in depth** with reliable backups and tested restores to limit impact if a deletion occurs.", + "Url": "https://hub.prowler.com/check/rds_instance_deletion_protection" } }, - "Categories": [], + "Categories": [ + "resilience" + ], "DependsOn": [], "RelatedTo": [], "Notes": "" diff --git a/prowler/providers/aws/services/rds/rds_instance_deprecated_engine_version/rds_instance_deprecated_engine_version.metadata.json b/prowler/providers/aws/services/rds/rds_instance_deprecated_engine_version/rds_instance_deprecated_engine_version.metadata.json index 378b8e0cac..0346756c88 100644 --- a/prowler/providers/aws/services/rds/rds_instance_deprecated_engine_version/rds_instance_deprecated_engine_version.metadata.json +++ b/prowler/providers/aws/services/rds/rds_instance_deprecated_engine_version/rds_instance_deprecated_engine_version.metadata.json @@ -1,30 +1,38 @@ { "Provider": "aws", "CheckID": "rds_instance_deprecated_engine_version", - "CheckTitle": "Check if RDS instance is using a supported engine version", - "CheckType": [], + "CheckTitle": "RDS instance uses a supported engine version", + "CheckType": [ + "Software and Configuration Checks/Patch Management", + "Software and Configuration Checks/AWS Security Best Practices" + ], "ServiceName": "rds", "SubServiceName": "", - "ResourceIdTemplate": "arn:aws:rds:region:account-id:db-instance", - "Severity": "medium", + "ResourceIdTemplate": "", + "Severity": "high", "ResourceType": "AwsRdsDbInstance", "ResourceGroup": "database", - "Description": "Check if RDS is using a supported engine version for MariaDB, MySQL and PostgreSQL", - "Risk": "If not enabled RDS instances may be vulnerable to security issues", - "RelatedUrl": "https://docs.aws.amazon.com/cli/latest/reference/rds/describe-db-engine-versions.html", + "Description": "**RDS DB instances** use a **supported, non-deprecated engine version** for MariaDB, MySQL, or PostgreSQL. The instance's `engine` and `engine_version` are evaluated against versions currently supported in the region.", + "Risk": "Deprecated engine versions lack security fixes and support, enabling exploitation of known flaws impacting **confidentiality** and **integrity**. **Availability** can suffer from forced maintenance or failed upgrades.\n- Unpatched CVEs\n- TLS/client incompatibilities\n- Replication or backup/restore issues", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://docs.aws.amazon.com/cli/latest/reference/rds/describe-db-engine-versions.html" + ], "Remediation": { "Code": { - "CLI": "aws rds describe-db-engine-versions --engine '", - "NativeIaC": "", - "Other": "", - "Terraform": "" + "CLI": "aws rds modify-db-instance --db-instance-identifier --engine-version --allow-major-version-upgrade --apply-immediately", + "NativeIaC": "```yaml\n# CloudFormation: upgrade RDS engine version for an existing instance\nResources:\n :\n Type: AWS::RDS::DBInstance\n Properties:\n DBInstanceIdentifier: \n Engine: \n DBInstanceClass: db.t3.micro\n EngineVersion: # CRITICAL: move to a supported engine version\n AllowMajorVersionUpgrade: true # CRITICAL: required if upgrading major version\n ApplyImmediately: true # CRITICAL: apply change now to pass the check\n```", + "Other": "1. In the AWS Console, go to RDS > Databases\n2. Select the DB instance\n3. Click Modify\n4. Under DB engine version, select a supported version\n5. If moving to a new major version, check Allow major version upgrade\n6. Check Apply immediately\n7. Click Continue, then Modify DB instance", + "Terraform": "```hcl\n# Upgrade RDS engine version\nresource \"aws_db_instance\" \"\" {\n identifier = \"\"\n engine = \"\"\n instance_class = \"db.t3.micro\"\n allocated_storage = 20\n\n engine_version = \"\" # CRITICAL: use a supported version\n allow_major_version_upgrade = true # CRITICAL: needed for major upgrades\n apply_immediately = true # CRITICAL: apply now to pass the check\n}\n```" }, "Recommendation": { - "Text": "Ensure all the RDS instances are using a supported engine version", - "Url": "https://docs.aws.amazon.com/cli/latest/reference/rds/describe-db-engine-versions.html" + "Text": "Standardize on **supported engine versions** and keep them current.\n- Plan and test upgrades; back up and define rollback\n- Enable `AutoMinorVersionUpgrade` where acceptable\n- Monitor deprecation notices and upgrade before EoS\n- Enforce **least privilege** to limit blast radius during incidents", + "Url": "https://hub.prowler.com/check/rds_instance_deprecated_engine_version" } }, - "Categories": [], + "Categories": [ + "vulnerabilities" + ], "DependsOn": [], "RelatedTo": [], "Notes": "" diff --git a/prowler/providers/aws/services/rds/rds_instance_enhanced_monitoring_enabled/rds_instance_enhanced_monitoring_enabled.metadata.json b/prowler/providers/aws/services/rds/rds_instance_enhanced_monitoring_enabled/rds_instance_enhanced_monitoring_enabled.metadata.json index f465173ae1..24dabf0f6a 100644 --- a/prowler/providers/aws/services/rds/rds_instance_enhanced_monitoring_enabled/rds_instance_enhanced_monitoring_enabled.metadata.json +++ b/prowler/providers/aws/services/rds/rds_instance_enhanced_monitoring_enabled/rds_instance_enhanced_monitoring_enabled.metadata.json @@ -1,30 +1,39 @@ { "Provider": "aws", "CheckID": "rds_instance_enhanced_monitoring_enabled", - "CheckTitle": "Check if RDS instances has enhanced monitoring enabled.", - "CheckType": [], + "CheckTitle": "RDS instance has enhanced monitoring enabled", + "CheckType": [ + "Software and Configuration Checks/AWS Security Best Practices/Runtime Behavior Analysis", + "Software and Configuration Checks/Industry and Regulatory Standards/AWS Foundational Security Best Practices" + ], "ServiceName": "rds", "SubServiceName": "", - "ResourceIdTemplate": "arn:aws:rds:region:account-id:db-instance", + "ResourceIdTemplate": "", "Severity": "low", "ResourceType": "AwsRdsDbInstance", "ResourceGroup": "database", - "Description": "Check if RDS instances has enhanced monitoring enabled.", - "Risk": "A smaller monitoring interval results in more frequent reporting of OS metrics.", - "RelatedUrl": "https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/USER_Monitoring.OS.html", + "Description": "**RDS DB instances** are evaluated for **Enhanced Monitoring** being enabled, which publishes real-time **OS-level metrics** (CPU, memory, disk, network) to CloudWatch Logs for each instance.", + "Risk": "Without **Enhanced Monitoring**, you lack **real-time OS telemetry**, delaying detection of resource saturation and abnormal activity.\n\nThis raises MTTR and risks **availability** impacts (timeouts, failovers), reduces **integrity** assurance during incidents, and weakens forensic visibility.", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/USER_Monitoring.OS.html", + "https://support.icompaas.com/support/solutions/articles/62000233699-ensu-rds-instances-has-enhanced-monitoring-enabled" + ], "Remediation": { "Code": { - "CLI": "aws rds create-db-instance --db-instance-identifier --db-instance-class --engine --storage-encrypted true", - "NativeIaC": "", - "Other": "", - "Terraform": "https://docs.prowler.com/checks/aws/logging-policies/ensure-that-enhanced-monitoring-is-enabled-for-amazon-rds-instances#terraform" + "CLI": "aws rds modify-db-instance --db-instance-identifier --monitoring-interval 60 --monitoring-role-arn ", + "NativeIaC": "```yaml\n# CloudFormation: enable Enhanced Monitoring on an existing RDS instance\nResources:\n :\n Type: AWS::RDS::DBInstance\n Properties:\n DBInstanceIdentifier: \n MonitoringRoleArn: # CRITICAL: IAM role RDS uses to publish OS metrics to CloudWatch Logs\n MonitoringInterval: 60 # CRITICAL: >0 enables Enhanced Monitoring (seconds)\n```", + "Other": "1. In the AWS Console, go to RDS > Databases and select the DB instance\n2. Click Modify\n3. In Monitoring, check Enable Enhanced Monitoring and set Granularity to any non-zero value (e.g., 60 seconds)\n4. Set Monitoring role to Default (creates rds-monitoring-role) or select an existing role\n5. Click Continue, then Modify DB instance to apply", + "Terraform": "```hcl\n# Enable Enhanced Monitoring on an existing RDS instance\nresource \"aws_db_instance\" \"\" {\n # ...existing required configuration...\n monitoring_role_arn = \"\" # CRITICAL: Role for publishing OS metrics\n monitoring_interval = 60 # CRITICAL: >0 enables Enhanced Monitoring (seconds)\n}\n```" }, "Recommendation": { - "Text": "To use Enhanced Monitoring, you must create an IAM role, and then enable Enhanced Monitoring.", - "Url": "https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/USER_Monitoring.OS.html" + "Text": "Enable **Enhanced Monitoring** on RDS, using a `>0s` collection interval aligned to workload and cost. Assign a **least-privilege** role for log delivery, and apply **defense in depth** by centralizing logs, setting **alerts** on key OS metrics, and defining **retention** to support incident response and trend analysis.", + "Url": "https://hub.prowler.com/check/rds_instance_enhanced_monitoring_enabled" } }, - "Categories": [], + "Categories": [ + "logging" + ], "DependsOn": [], "RelatedTo": [], "Notes": "" diff --git a/prowler/providers/aws/services/rds/rds_instance_event_subscription_parameter_groups/rds_instance_event_subscription_parameter_groups.metadata.json b/prowler/providers/aws/services/rds/rds_instance_event_subscription_parameter_groups/rds_instance_event_subscription_parameter_groups.metadata.json index b68cd7366d..73a20fe710 100644 --- a/prowler/providers/aws/services/rds/rds_instance_event_subscription_parameter_groups/rds_instance_event_subscription_parameter_groups.metadata.json +++ b/prowler/providers/aws/services/rds/rds_instance_event_subscription_parameter_groups/rds_instance_event_subscription_parameter_groups.metadata.json @@ -1,30 +1,39 @@ { "Provider": "aws", "CheckID": "rds_instance_event_subscription_parameter_groups", - "CheckTitle": "Check if RDS Parameter Group events are subscribed.", - "CheckType": [], + "CheckTitle": "RDS DB parameter group event subscription is enabled and subscribes to configuration change events or all categories", + "CheckType": [ + "Software and Configuration Checks/AWS Security Best Practices", + "Software and Configuration Checks/Industry and Regulatory Standards/AWS Foundational Security Best Practices" + ], "ServiceName": "rds", "SubServiceName": "", - "ResourceIdTemplate": "arn:aws:rds:region:account-id:account", + "ResourceIdTemplate": "", "Severity": "low", - "ResourceType": "AwsAccount", - "ResourceGroup": "governance", - "Description": "Ensure that Amazon RDS event notification subscriptions are enabled for database parameter groups events.", - "Risk": "Amazon RDS event subscriptions for database parameter groups are designed to provide incident notification of events that may affect the security, availability, and reliability of the RDS database instances associated with these parameter groups.", - "RelatedUrl": "https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/USER_Events.html", + "ResourceType": "AwsRdsEventSubscription", + "ResourceGroup": "database", + "Description": "**RDS event subscriptions** for **DB parameter groups** notify on `configuration change` events (or all categories) when the subscription is enabled", + "Risk": "Missing alerts on parameter changes erodes visibility. Attackers or mistakes can lower TLS requirements, disable auditing, or relax auth, enabling data exposure (**confidentiality**), unauthorized writes (**integrity**), or outages from unsafe settings (**availability**).", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/USER_Events.html", + "https://docs.aws.amazon.com/securityhub/latest/userguide/rds-controls.html#rds-21" + ], "Remediation": { "Code": { - "CLI": "aws rds create-event-subscription --source-type db-instance --event-categories 'configuration change' --sns-topic-arn ", - "NativeIaC": "", - "Other": "https://docs.aws.amazon.com/securityhub/latest/userguide/rds-controls.html#rds-21", - "Terraform": "" + "CLI": "aws rds create-event-subscription --subscription-name --sns-topic-arn --source-type db-parameter-group --event-categories \"configuration change\" --enabled", + "NativeIaC": "```yaml\nResources:\n :\n Type: AWS::RDS::EventSubscription\n Properties:\n SnsTopicArn: \n SourceType: db-parameter-group # Critical: targets DB parameter group events\n EventCategories:\n - configuration change # Critical: subscribes to configuration change events\n Enabled: true # Critical: subscription must be enabled\n```", + "Other": "1. In the AWS Console, go to Amazon RDS > Event subscriptions\n2. Click Create event subscription\n3. Send notifications to: select an existing SNS topic\n4. Source type: Parameter groups\n5. Event categories: select Configuration change (or choose All event categories)\n6. Ensure Enabled is On\n7. Click Create", + "Terraform": "```hcl\nresource \"aws_db_event_subscription\" \"\" {\n name = \"\"\n sns_topic = \"\"\n source_type = \"db-parameter-group\" # Critical: target DB parameter groups\n event_categories = [\"configuration change\"] # Critical: include configuration change events\n}\n```" }, "Recommendation": { - "Text": "To subscribe to RDS instance event notifications, see Subscribing to Amazon RDS event notification in the Amazon RDS User Guide.", - "Url": "https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/USER_Events.Subscribing.html" + "Text": "Create and maintain an **SNS-backed event subscription** for **DB parameter groups** that includes `configuration change` (or all) and keep it enabled.\n\n- Apply **least privilege** to SNS topics\n- Route to on-call/SIEM and test alerts\n- Enforce change control and monitoring across all Regions", + "Url": "https://hub.prowler.com/check/rds_instance_event_subscription_parameter_groups" } }, - "Categories": [], + "Categories": [ + "logging" + ], "DependsOn": [], "RelatedTo": [], "Notes": "" diff --git a/prowler/providers/aws/services/rds/rds_instance_event_subscription_security_groups/rds_instance_event_subscription_security_groups.metadata.json b/prowler/providers/aws/services/rds/rds_instance_event_subscription_security_groups/rds_instance_event_subscription_security_groups.metadata.json index 9e59493988..1019190f08 100644 --- a/prowler/providers/aws/services/rds/rds_instance_event_subscription_security_groups/rds_instance_event_subscription_security_groups.metadata.json +++ b/prowler/providers/aws/services/rds/rds_instance_event_subscription_security_groups/rds_instance_event_subscription_security_groups.metadata.json @@ -1,30 +1,39 @@ { "Provider": "aws", "CheckID": "rds_instance_event_subscription_security_groups", - "CheckTitle": "Check if RDS Security Group events are subscribed.", - "CheckType": [], + "CheckTitle": "RDS event subscription for DB security groups is enabled for configuration change and failure events", + "CheckType": [ + "Software and Configuration Checks/AWS Security Best Practices", + "Software and Configuration Checks/Industry and Regulatory Standards/AWS Foundational Security Best Practices" + ], "ServiceName": "rds", "SubServiceName": "", - "ResourceIdTemplate": "arn:aws:rds:region:account-id:es", + "ResourceIdTemplate": "", "Severity": "medium", "ResourceType": "AwsRdsEventSubscription", "ResourceGroup": "database", - "Description": "Ensure that Amazon RDS event notification subscriptions are enabled for database security groups events.", - "Risk": "Amazon RDS event subscriptions for database security groups are designed to provide incident notification of events that may affect the security, availability, and reliability of the RDS database instances associated with these security groups.", - "RelatedUrl": "https://docs.aws.amazon.com/securityhub/latest/userguide/rds-controls.html#rds-22", + "Description": "**RDS event subscriptions** are evaluated for **database security group** events. The check expects an enabled subscription with source type `db-security-group` that includes the `configuration change` and `failure` event categories.", + "Risk": "Missing alerts on **security group changes** or **failures** reduces visibility and slows response. Undetected rule changes can expose databases, while unnoticed failures prolong outages, impacting **confidentiality** and **availability** through unauthorized access or service disruption.", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://www.trendmicro.com/cloudoneconformity/knowledge-base/aws/RDS/rds-db-security-groups-events.html#", + "https://docs.aws.amazon.com/securityhub/latest/userguide/rds-controls.html#rds-22" + ], "Remediation": { "Code": { - "CLI": "", - "NativeIaC": "https://www.trendmicro.com/cloudoneconformity/knowledge-base/aws/RDS/rds-db-security-groups-events.html#", - "Other": "https://www.trendmicro.com/cloudoneconformity/knowledge-base/aws/RDS/rds-db-security-groups-events.html#", - "Terraform": "https://www.trendmicro.com/cloudoneconformity/knowledge-base/aws/RDS/rds-db-security-groups-events.html#" + "CLI": "aws rds create-event-subscription --subscription-name --sns-topic-arn --source-type db-security-group --event-categories \"configuration change\" \"failure\"", + "NativeIaC": "```yaml\nResources:\n :\n Type: AWS::RDS::EventSubscription\n Properties:\n SnsTopicArn: \n SourceType: db-security-group # Critical: subscribe to DB security group events\n EventCategories: # Critical: required categories\n - configuration change\n - failure\n```", + "Other": "1. Open the AWS Console > RDS > Event subscriptions\n2. Click Create event subscription\n3. Set Name to and select an existing SNS topic\n4. Set Source type to Security group\n5. Under Event categories, select Configuration change and Failure\n6. Leave Targets as All security groups (no specific IDs)\n7. Ensure Enabled is On and click Create", + "Terraform": "```hcl\nresource \"aws_db_event_subscription\" \"\" {\n name = \"\"\n sns_topic = \"\"\n source_type = \"db-security-group\" # Critical: DB security group events\n event_categories = [\"configuration change\", \"failure\"] # Critical: required categories\n}\n```" }, "Recommendation": { - "Text": "To subscribe to RDS instance event notifications, see Subscribing to Amazon RDS event notification in the Amazon RDS User Guide.", - "Url": "https://docs.aws.amazon.com/securityhub/latest/userguide/rds-controls.html#rds-22" + "Text": "Create or update an **RDS event subscription** for source type `db-security-group` including `configuration change` and `failure`. Route alerts to monitored channels, restrict topic access (**least privilege**), integrate with **incident response**, and enforce change control and **separation of duties** for security group updates.", + "Url": "https://hub.prowler.com/check/rds_instance_event_subscription_security_groups" } }, - "Categories": [], + "Categories": [ + "logging" + ], "DependsOn": [], "RelatedTo": [], "Notes": "" diff --git a/prowler/providers/aws/services/rds/rds_instance_iam_authentication_enabled/rds_instance_iam_authentication_enabled.metadata.json b/prowler/providers/aws/services/rds/rds_instance_iam_authentication_enabled/rds_instance_iam_authentication_enabled.metadata.json index f7c6f6af53..be4c4fa856 100644 --- a/prowler/providers/aws/services/rds/rds_instance_iam_authentication_enabled/rds_instance_iam_authentication_enabled.metadata.json +++ b/prowler/providers/aws/services/rds/rds_instance_iam_authentication_enabled/rds_instance_iam_authentication_enabled.metadata.json @@ -1,30 +1,41 @@ { "Provider": "aws", "CheckID": "rds_instance_iam_authentication_enabled", - "CheckTitle": "Check if RDS instances have IAM authentication enabled.", - "CheckType": [], + "CheckTitle": "RDS instance has IAM database authentication enabled", + "CheckType": [ + "Software and Configuration Checks/AWS Security Best Practices", + "Software and Configuration Checks/Industry and Regulatory Standards/AWS Foundational Security Best Practices", + "TTPs/Credential Access" + ], "ServiceName": "rds", "SubServiceName": "", - "ResourceIdTemplate": "arn:aws:rds:region:account-id:db-instance", + "ResourceIdTemplate": "", "Severity": "medium", "ResourceType": "AwsRdsDbInstance", "ResourceGroup": "database", - "Description": "Check if RDS instances have IAM authentication enabled.", - "Risk": "Ensure that the IAM Database Authentication feature is enabled for your RDS database instances in order to use the Identity and Access Management (IAM) service to manage database access to your MySQL and PostgreSQL database instances. With this feature enabled, you don't have to use a password when you connect to your MySQL/PostgreSQL database, instead you can use an authentication token. An authentication token is a unique string of characters with a lifetime of 15 minutes that Amazon RDS generates on your request. IAM Database Authentication removes the need of storing user credentials within the database configuration, because authentication is managed externally using Amazon IAM.", - "RelatedUrl": "https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/UsingWithRDS.IAMDBAuth.Enabling.html", + "Description": "**RDS DB instances** using MySQL, MariaDB, or PostgreSQL engines (including Aurora variants) have **IAM database authentication** enabled at the instance level or, when part of a cluster, evaluated for cluster-level enablement.", + "Risk": "Absent **IAM-based, short-lived tokens**, databases rely on **static passwords** in code and configs, increasing theft and reuse.\n\nCompromised DB creds enable unauthorized queries, leading to **data exfiltration** and tampering, and weaken **centralized access control** and rotation.", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://docs.aws.amazon.com/securityhub/latest/userguide/rds-controls.html#rds-10", + "https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/UsingWithRDS.IAMDBAuth.Enabling.html", + "https://www.trendmicro.com/cloudoneconformity/knowledge-base/aws/RDS/iam-database-authentication.html#" + ], "Remediation": { "Code": { - "CLI": "aws rds modify-db-instance --region --db-instance-identifier --enable-iam-database-authentication --apply-immediately", - "NativeIaC": "https://www.trendmicro.com/cloudoneconformity/knowledge-base/aws/RDS/iam-database-authentication.html#", - "Other": "https://docs.aws.amazon.com/securityhub/latest/userguide/rds-controls.html#rds-10", - "Terraform": "https://www.trendmicro.com/cloudoneconformity/knowledge-base/aws/RDS/iam-database-authentication.html#" + "CLI": "aws rds modify-db-instance --db-instance-identifier --enable-iam-database-authentication --apply-immediately", + "NativeIaC": "```yaml\n# CloudFormation: enable IAM DB authentication on an RDS instance\nResources:\n :\n Type: AWS::RDS::DBInstance\n Properties:\n DBInstanceIdentifier: \n EnableIAMDatabaseAuthentication: true # Critical: enables IAM DB auth to pass the check\n```", + "Other": "1. In the AWS console, go to RDS > Databases\n2. Select the DB instance and choose Modify\n3. Under Database authentication, select \"Password and IAM database authentication\"\n4. Choose Apply immediately and click Modify DB instance\n5. If the instance is part of an Aurora DB cluster: select the DB cluster instead, choose Modify, enable IAM database authentication, Apply immediately, then Modify", + "Terraform": "```hcl\n# Enable IAM DB authentication on an RDS instance\nresource \"aws_db_instance\" \"\" {\n iam_database_authentication_enabled = true # Critical: enables IAM DB auth to pass the check\n}\n```" }, "Recommendation": { - "Text": "Enable IAM authentication for supported RDS instances.", - "Url": "https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/UsingWithRDS.IAMDBAuth.Enabling.html" + "Text": "Enable **IAM database authentication** for supported engines and apply **least privilege** with scoped IAM policies. Prefer **short-lived tokens** over static DB passwords, enforce TLS, and phase out embedded credentials.\n\nMonitor authentication activity with audit logs for **defense in depth**.", + "Url": "https://hub.prowler.com/check/rds_instance_iam_authentication_enabled" } }, - "Categories": [], + "Categories": [ + "identity-access" + ], "DependsOn": [], "RelatedTo": [], "Notes": "" diff --git a/prowler/providers/aws/services/rds/rds_instance_inside_vpc/rds_instance_inside_vpc.metadata.json b/prowler/providers/aws/services/rds/rds_instance_inside_vpc/rds_instance_inside_vpc.metadata.json index 12aaa01559..c9f5fd2c96 100644 --- a/prowler/providers/aws/services/rds/rds_instance_inside_vpc/rds_instance_inside_vpc.metadata.json +++ b/prowler/providers/aws/services/rds/rds_instance_inside_vpc/rds_instance_inside_vpc.metadata.json @@ -1,32 +1,40 @@ { "Provider": "aws", "CheckID": "rds_instance_inside_vpc", - "CheckTitle": "Check if RDS instances are deployed within a VPC.", + "CheckTitle": "RDS instance is deployed in a VPC", "CheckType": [ - "Software and Configuration Checks, AWS Security Best Practices" + "Software and Configuration Checks/AWS Security Best Practices/Network Reachability", + "Software and Configuration Checks/Industry and Regulatory Standards/AWS Foundational Security Best Practices" ], "ServiceName": "rds", "SubServiceName": "", - "ResourceIdTemplate": "arn:aws:rds:region:account-id:db-instance", + "ResourceIdTemplate": "", "Severity": "high", "ResourceType": "AwsRdsDbInstance", "ResourceGroup": "database", - "Description": "Check if RDS instances are deployed within a VPC.", - "Risk": "If your RDS instances are not deployed within a VPC, they are not isolated from the public internet and are exposed to potential security threats. Deploying RDS instances within a VPC allows you to control inbound and outbound traffic to and from the instances, and provides an additional layer of security to your database instances.", - "RelatedUrl": "https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/USER_VPC.WorkingWithRDSInstanceinaVPC.html#USER_VPC.Subnets", + "Description": "**RDS DB instances** are assessed for **VPC placement** by the presence of a `vpc_id` indicating deployment within a VPC.\n\nInstances without this association are treated as outside VPC networking.", + "Risk": "Without VPC isolation, databases may expose internet-reachable endpoints and lack granular network controls. This degrades **confidentiality** and **availability** via scanning/brute force and data exfiltration, and threatens **integrity** through unauthorized connections and lateral movement.", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://docs.aws.amazon.com/securityhub/latest/userguide/rds-controls.html#rds-18", + "https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/Overview.DBInstance.Modifying.html", + "https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/USER_VPC.WorkingWithRDSInstanceinaVPC.html#USER_VPC.Subnets" + ], "Remediation": { "Code": { - "CLI": "aws rds modify-db-instance --db-instance-identifier --vpc-security-group-ids ", - "NativeIaC": "", - "Other": "https://docs.aws.amazon.com/securityhub/latest/userguide/rds-controls.html#rds-18", - "Terraform": "" + "CLI": "aws rds modify-db-instance --db-instance-identifier --db-subnet-group-name --apply-immediately", + "NativeIaC": "```yaml\n# CloudFormation: move RDS instance into a VPC by assigning a DB subnet group\nResources:\n :\n Type: AWS::RDS::DBSubnetGroup\n Properties:\n DBSubnetGroupDescription: \"subnets for rds\"\n SubnetIds:\n - # CRITICAL: Subnets in the target VPC\n - # CRITICAL: At least two AZs recommended\n\n :\n Type: AWS::RDS::DBInstance\n Properties:\n DBSubnetGroupName: !Ref # CRITICAL: Ensures the DB instance is deployed in a VPC\n```", + "Other": "1. In the AWS Console, go to RDS > Subnet groups and create/select a DB subnet group in the target VPC (with subnets in at least two AZs)\n2. Go to RDS > Databases, select the DB instance, click Modify\n3. Under Connectivity, set DB subnet group to the VPC subnet group from step 1 (select a VPC security group if prompted)\n4. Check Apply immediately and choose Continue > Modify DB instance", + "Terraform": "```hcl\n# Terraform: ensure RDS instance is in a VPC via DB subnet group\nresource \"aws_db_subnet_group\" \"\" {\n name = \"\"\n subnet_ids = [\n \"\", # CRITICAL: Subnets in the target VPC\n \"\"\n ]\n}\n\nresource \"aws_db_instance\" \"\" {\n identifier = \"\"\n db_subnet_group_name = aws_db_subnet_group..name # CRITICAL: Places instance in a VPC\n}\n```" }, "Recommendation": { - "Text": "Ensure that your RDS instances are deployed within a VPC to provide an additional layer of security to your database instances.", - "Url": "https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/Overview.DBInstance.Modifying.html" + "Text": "Deploy all RDS instances in a **VPC**, preferably in **private subnets**. Enforce **least privilege** with security groups, network ACLs, and restrictive routing. Use private connectivity (peering, VPN, Direct Connect), avoid public exposure, and apply **defense in depth** through segmentation and monitoring.", + "Url": "https://hub.prowler.com/check/rds_instance_inside_vpc" } }, - "Categories": [], + "Categories": [ + "trust-boundaries" + ], "DependsOn": [], "RelatedTo": [], "Notes": "" diff --git a/prowler/providers/aws/services/rds/rds_instance_integration_cloudwatch_logs/rds_instance_integration_cloudwatch_logs.metadata.json b/prowler/providers/aws/services/rds/rds_instance_integration_cloudwatch_logs/rds_instance_integration_cloudwatch_logs.metadata.json index ee5f8b282b..4d4834143d 100644 --- a/prowler/providers/aws/services/rds/rds_instance_integration_cloudwatch_logs/rds_instance_integration_cloudwatch_logs.metadata.json +++ b/prowler/providers/aws/services/rds/rds_instance_integration_cloudwatch_logs/rds_instance_integration_cloudwatch_logs.metadata.json @@ -1,27 +1,35 @@ { "Provider": "aws", "CheckID": "rds_instance_integration_cloudwatch_logs", - "CheckTitle": "Check if RDS instances is integrated with CloudWatch Logs.", - "CheckType": [], + "CheckTitle": "RDS instance exports logs to CloudWatch Logs", + "CheckType": [ + "Software and Configuration Checks/AWS Security Best Practices", + "Software and Configuration Checks/Industry and Regulatory Standards/AWS Foundational Security Best Practices" + ], "ServiceName": "rds", "SubServiceName": "", - "ResourceIdTemplate": "arn:aws:rds:region:account-id:db-instance", + "ResourceIdTemplate": "", "Severity": "medium", "ResourceType": "AwsRdsDbInstance", "ResourceGroup": "database", - "Description": "Check if RDS instances is integrated with CloudWatch Logs.", - "Risk": "If logs are not enabled, monitoring of service use and threat analysis is not possible.", - "RelatedUrl": "https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/publishing_cloudwatchlogs.html", + "Description": "**RDS DB instances** are configured to **publish database logs** to **CloudWatch Logs** (e.g., `error`, `general`, `slowquery`, `audit`).\n\nThe evaluation identifies instances that have log exports enabled to a CloudWatch log group.", + "Risk": "Without centralized RDS logs, database activity lacks visibility, hindering detection and response. Credential misuse, SQL injection, data exfiltration, and privilege abuse may go unnoticed, risking **confidentiality** and **integrity**. Unseen errors and slow queries can lead to outages, impacting **availability**.", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://www.trendmicro.com/cloudoneconformity/knowledge-base/aws/RDS/log-exports.html", + "https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/publishing_cloudwatchlogs.html", + "https://repost.aws/knowledge-center/rds-aurora-mysql-logs-cloudwatch" + ], "Remediation": { "Code": { - "CLI": "aws rds modify-db-instance --db-instance-identifier --cloudwatch-logs-export-configuration {'EnableLogTypes':['audit',error','general','slowquery']} --apply-immediately", - "NativeIaC": "", - "Other": "https://www.trendmicro.com/cloudoneconformity/knowledge-base/aws/RDS/log-exports.html", - "Terraform": "https://docs.prowler.com/checks/aws/iam-policies/ensure-that-respective-logs-of-amazon-relational-database-service-amazon-rds-are-enabled#terraform" + "CLI": "aws rds modify-db-instance --db-instance-identifier --cloudwatch-logs-export-configuration '{\"EnableLogTypes\":[\"\"]}'", + "NativeIaC": "```yaml\nResources:\n :\n Type: AWS::RDS::DBInstance\n Properties:\n DBInstanceIdentifier: \n # Critical: enabling at least one log type exports it to CloudWatch Logs and makes the check PASS\n EnableCloudwatchLogsExports:\n - \n```", + "Other": "1. Open AWS Console > RDS > Databases\n2. Select your DB instance and choose Modify\n3. In Log exports, select at least one supported log type (e.g., error/general/slowquery/audit/postgresql/alert)\n4. Choose Continue, then Modify DB instance", + "Terraform": "```hcl\nresource \"aws_db_instance\" \"\" {\n identifier = \"\"\n # Critical: export at least one supported log type to CloudWatch Logs to pass the check\n enabled_cloudwatch_logs_exports = [\"\"]\n}\n```" }, "Recommendation": { - "Text": "Use CloudWatch Logs to perform real-time analysis of the log data. Create alarms and view metrics.", - "Url": "https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/publishing_cloudwatchlogs.html" + "Text": "Enable export of relevant RDS logs to **CloudWatch Logs** (`error`, `general`, `slowquery`, `audit`) and standardize across engines. Enforce **least privilege** on log access, set retention, and define metrics/alarms for critical patterns. Integrate with a SIEM. Apply **separation of duties** and **defense in depth** to protect log integrity and monitoring.", + "Url": "https://hub.prowler.com/check/rds_instance_integration_cloudwatch_logs" } }, "Categories": [ diff --git a/prowler/providers/aws/services/rds/rds_instance_minor_version_upgrade_enabled/rds_instance_minor_version_upgrade_enabled.metadata.json b/prowler/providers/aws/services/rds/rds_instance_minor_version_upgrade_enabled/rds_instance_minor_version_upgrade_enabled.metadata.json index 8951a849aa..8a11bbeb38 100644 --- a/prowler/providers/aws/services/rds/rds_instance_minor_version_upgrade_enabled/rds_instance_minor_version_upgrade_enabled.metadata.json +++ b/prowler/providers/aws/services/rds/rds_instance_minor_version_upgrade_enabled/rds_instance_minor_version_upgrade_enabled.metadata.json @@ -1,30 +1,38 @@ { "Provider": "aws", "CheckID": "rds_instance_minor_version_upgrade_enabled", - "CheckTitle": "Ensure RDS instances have minor version upgrade enabled.", - "CheckType": [], + "CheckTitle": "RDS instance has minor version upgrade enabled", + "CheckType": [ + "Software and Configuration Checks/Patch Management" + ], "ServiceName": "rds", "SubServiceName": "", - "ResourceIdTemplate": "arn:aws:rds:region:account-id:db-instance", - "Severity": "low", + "ResourceIdTemplate": "", + "Severity": "medium", "ResourceType": "AwsRdsDbInstance", "ResourceGroup": "database", - "Description": "Ensure RDS instances have minor version upgrade enabled.", - "Risk": "Auto Minor Version Upgrade is a feature that you can enable to have your database automatically upgraded when a new minor database engine version is available. Minor version upgrades often patch security vulnerabilities and fix bugs and therefore should be applied.", - "RelatedUrl": "https://aws.amazon.com/blogs/database/best-practices-for-upgrading-amazon-rds-to-major-and-minor-versions-of-postgresql/", + "Description": "**RDS DB instances** are evaluated for the `auto_minor_version_upgrade` setting that enables **automatic minor engine updates** during maintenance windows.", + "Risk": "Without automatic minor upgrades, databases miss **security patches**, leaving known vulnerabilities exploitable and risking **unauthorized data access**. Unapplied fixes can cause **data corruption** and outages, harming **integrity** and **availability**.", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://aws.amazon.com/blogs/database/best-practices-for-upgrading-amazon-rds-to-major-and-minor-versions-of-postgresql/", + "https://www.trendmicro.com/cloudoneconformity/knowledge-base/aws/RDS/rds-auto-minor-version-upgrade.html" + ], "Remediation": { "Code": { "CLI": "aws rds modify-db-instance --db-instance-identifier --auto-minor-version-upgrade --apply-immediately", - "NativeIaC": "https://docs.prowler.com/checks/aws/general-policies/ensure-aws-db-instance-gets-all-minor-upgrades-automatically#cloudformation", - "Other": "https://www.trendmicro.com/cloudoneconformity/knowledge-base/aws/RDS/rds-auto-minor-version-upgrade.html", - "Terraform": "https://docs.prowler.com/checks/aws/general-policies/ensure-aws-db-instance-gets-all-minor-upgrades-automatically#terraform" + "NativeIaC": "```yaml\n# CloudFormation: Enable auto minor version upgrades on an RDS instance\nResources:\n :\n Type: AWS::RDS::DBInstance\n Properties:\n DBInstanceIdentifier: \n DBInstanceClass: db.t3.micro\n Engine: mysql\n MasterUsername: \n MasterUserPassword: \n AllocatedStorage: '20'\n AutoMinorVersionUpgrade: true # Critical: ensures RDS applies minor engine updates automatically\n```", + "Other": "1. In the AWS Console, go to RDS > Databases\n2. Select the DB instance and click Modify\n3. Find \"Auto minor version upgrade\" and set it to Enable\n4. Click Continue, check Apply immediately, then click Modify DB instance", + "Terraform": "```hcl\n# Enable auto minor version upgrades on an RDS instance\nresource \"aws_db_instance\" \"\" {\n allocated_storage = 20\n engine = \"mysql\"\n instance_class = \"db.t3.micro\"\n username = \"\"\n password = \"\"\n auto_minor_version_upgrade = true # Critical: turns on automatic minor engine upgrades\n}\n```" }, "Recommendation": { - "Text": "Enable auto minor version upgrade for all databases and environments.", - "Url": "https://aws.amazon.com/blogs/database/best-practices-for-upgrading-amazon-rds-to-major-and-minor-versions-of-postgresql/" + "Text": "Enable `auto_minor_version_upgrade` on RDS instances so minor releases are applied promptly. Use maintenance windows and stage testing to limit impact. Follow **defense in depth** and **least privilege**; keep reliable backups and Multi-AZ to preserve continuity if upgrades require rollback.", + "Url": "https://hub.prowler.com/check/rds_instance_minor_version_upgrade_enabled" } }, - "Categories": [], + "Categories": [ + "vulnerabilities" + ], "DependsOn": [], "RelatedTo": [], "Notes": "" diff --git a/prowler/providers/aws/services/rds/rds_instance_multi_az/rds_instance_multi_az.metadata.json b/prowler/providers/aws/services/rds/rds_instance_multi_az/rds_instance_multi_az.metadata.json index 0fc8188164..b2430589a7 100644 --- a/prowler/providers/aws/services/rds/rds_instance_multi_az/rds_instance_multi_az.metadata.json +++ b/prowler/providers/aws/services/rds/rds_instance_multi_az/rds_instance_multi_az.metadata.json @@ -1,31 +1,37 @@ { "Provider": "aws", "CheckID": "rds_instance_multi_az", - "CheckTitle": "Check if RDS instances have multi-AZ enabled.", - "CheckType": [], + "CheckTitle": "RDS instance has Multi-AZ enabled", + "CheckType": [ + "Software and Configuration Checks/AWS Security Best Practices" + ], "ServiceName": "rds", "SubServiceName": "", - "ResourceIdTemplate": "arn:aws:rds:region:account-id:db-instance", + "ResourceIdTemplate": "", "Severity": "medium", "ResourceType": "AwsRdsDbInstance", "ResourceGroup": "database", - "Description": "Check if RDS instances have multi-AZ enabled.", - "Risk": "In case of failure, with a single-AZ deployment configuration, should an availability zone specific database failure occur, Amazon RDS can not automatically fail over to the standby availability zone.", - "RelatedUrl": "https://aws.amazon.com/rds/features/multi-az/", + "Description": "**RDS DB instances** are evaluated for **Multi-AZ** configuration, either enabled on the instance or inherited from the associated DB cluster.", + "Risk": "Without **Multi-AZ**, an Availability Zone or instance failure can cause extended downtime and rely on restores, risking loss of recent writes. This degrades **availability** and may affect **integrity**, interrupting applications and breaching SLAs.", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://www.trendmicro.com/cloudoneconformity/knowledge-base/aws/RDS/rds-multi-az.html", + "https://aws.amazon.com/rds/features/multi-az/" + ], "Remediation": { "Code": { - "CLI": "aws rds create-db-instance --db-instance-identifier --multi-az true", - "NativeIaC": "https://docs.prowler.com/checks/aws/general-policies/general_73#cloudformation", - "Other": "https://www.trendmicro.com/cloudoneconformity/knowledge-base/aws/RDS/rds-multi-az.html", - "Terraform": "https://docs.prowler.com/checks/aws/general-policies/general_73#terraform" + "CLI": "aws rds modify-db-instance --db-instance-identifier --multi-az --apply-immediately", + "NativeIaC": "```yaml\n# CloudFormation: enable Multi-AZ on an existing RDS DB instance\nResources:\n RDSInstance:\n Type: AWS::RDS::DBInstance\n Properties:\n MultiAZ: true # Critical: enables Multi-AZ to pass the check\n```", + "Other": "1. Open the AWS Management Console and go to RDS > Databases\n2. Select the affected DB instance and click Modify\n3. Under Availability & durability, set Multi-AZ deployment to Enabled (create a standby)\n4. Check Apply immediately\n5. Click Continue, then Modify DB instance\n6. Wait until status is Available and Multi-AZ shows Yes", + "Terraform": "```hcl\n# Enable Multi-AZ on an RDS DB instance\nresource \"aws_db_instance\" \"example\" {\n multi_az = true # Critical: enables Multi-AZ to pass the check\n}\n```" }, "Recommendation": { - "Text": "Enable multi-AZ deployment for production databases.", - "Url": "https://aws.amazon.com/rds/features/multi-az/" + "Text": "Apply fault-tolerance and redundancy principles: enable **Multi-AZ** for production RDS workloads. Choose one standby or two readable standbys based on RTO/RPO and performance needs. Regularly test failover, monitor configuration drift, and allow exceptions only with documented, risk-based approval.", + "Url": "https://hub.prowler.com/check/rds_instance_multi_az" } }, "Categories": [ - "redundancy" + "resilience" ], "DependsOn": [], "RelatedTo": [], diff --git a/prowler/providers/aws/services/rds/rds_instance_no_public_access/rds_instance_no_public_access.metadata.json b/prowler/providers/aws/services/rds/rds_instance_no_public_access/rds_instance_no_public_access.metadata.json index 390504cca9..6f64cf9d90 100644 --- a/prowler/providers/aws/services/rds/rds_instance_no_public_access/rds_instance_no_public_access.metadata.json +++ b/prowler/providers/aws/services/rds/rds_instance_no_public_access/rds_instance_no_public_access.metadata.json @@ -1,27 +1,37 @@ { "Provider": "aws", "CheckID": "rds_instance_no_public_access", - "CheckTitle": "Ensure there are no Public Accessible RDS instances.", - "CheckType": [], + "CheckTitle": "RDS instance is not publicly exposed to the Internet", + "CheckType": [ + "Software and Configuration Checks/AWS Security Best Practices/Network Reachability", + "Software and Configuration Checks/Industry and Regulatory Standards/AWS Foundational Security Best Practices", + "Software and Configuration Checks/Industry and Regulatory Standards/CIS AWS Foundations Benchmark", + "TTPs/Initial Access" + ], "ServiceName": "rds", "SubServiceName": "", - "ResourceIdTemplate": "arn:aws:rds:region:account-id:db-instance", + "ResourceIdTemplate": "", "Severity": "critical", "ResourceType": "AwsRdsDbInstance", "ResourceGroup": "database", - "Description": "Ensure there are no Public Accessible RDS instances.", - "Risk": "Publicly accessible databases could expose sensitive data to bad actors.", - "RelatedUrl": "https://docs.aws.amazon.com/config/latest/developerguide/rds-instance-public-access-check.html", + "Description": "**RDS DB instances** are assessed for **internet exposure** using the `PubliclyAccessible` setting, security group ingress to the DB port from any address, and whether subnets are **public**. Instances that combine an internet-facing endpoint, open ingress, and public subnets are identified.", + "Risk": "An internet-reachable database invites:\n- Brute-force and vulnerability probing, risking **availability** and **integrity**\n- Unauthorized queries and dumps leading to **confidentiality** loss\n\nCompromise can enable **lateral movement** and persistence within the VPC.", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://docs.aws.amazon.com/config/latest/developerguide/rds-instance-public-access-check.html", + "https://www.trendmicro.com/cloudoneconformity/knowledge-base/aws/RDS/rds-publicly-accessible.html", + "https://docs.aws.amazon.com/securityhub/latest/userguide/rds-controls.html" + ], "Remediation": { "Code": { "CLI": "aws rds modify-db-instance --db-instance-identifier --no-publicly-accessible --apply-immediately", - "NativeIaC": "https://docs.prowler.com/checks/aws/public-policies/public_2#cloudformation", - "Other": "https://www.trendmicro.com/cloudoneconformity/knowledge-base/aws/RDS/rds-publicly-accessible.html", - "Terraform": "https://docs.prowler.com/checks/aws/public-policies/public_2#terraform" + "NativeIaC": "```yaml\n# CloudFormation: make the RDS instance private\nResources:\n :\n Type: AWS::RDS::DBInstance\n Properties:\n DBInstanceIdentifier: \n PubliclyAccessible: false # CRITICAL: disables public access so the instance is not Internet-facing\n```", + "Other": "1. In the AWS console, go to RDS > Databases and select your DB instance\n2. Click Modify\n3. In Connectivity (Connectivity & security), set Public access to No (Not publicly accessible)\n4. Choose Continue, select Apply immediately (or during the next window), then click Modify DB instance", + "Terraform": "```hcl\n# Ensure the RDS instance is not publicly accessible\nresource \"aws_db_instance\" \"\" {\n publicly_accessible = false # CRITICAL: disables public access (no public endpoint)\n}\n```" }, "Recommendation": { - "Text": "Using an AWS Config rule check for RDS public instances periodically and check there is a business reason for it.", - "Url": "https://docs.aws.amazon.com/config/latest/developerguide/rds-instance-public-access-check.html" + "Text": "Keep databases private by applying **least privilege** at the network layer:\n- Set `PubliclyAccessible` to `false`\n- Place instances in private subnets\n- Deny `0.0.0.0/0` and `::/0` on the DB port\n- Expose access via private endpoints, VPN, or an application tier/DB proxy\n\nAdopt **defense in depth** with monitoring and strong auth.", + "Url": "https://hub.prowler.com/check/rds_instance_no_public_access" } }, "Categories": [ diff --git a/prowler/providers/aws/services/rds/rds_instance_non_default_port/rds_instance_non_default_port.metadata.json b/prowler/providers/aws/services/rds/rds_instance_non_default_port/rds_instance_non_default_port.metadata.json index 98a80abaa7..73d731a471 100644 --- a/prowler/providers/aws/services/rds/rds_instance_non_default_port/rds_instance_non_default_port.metadata.json +++ b/prowler/providers/aws/services/rds/rds_instance_non_default_port/rds_instance_non_default_port.metadata.json @@ -1,29 +1,35 @@ { "Provider": "aws", "CheckID": "rds_instance_non_default_port", - "CheckTitle": "Check if RDS instances are using non-default ports.", + "CheckTitle": "RDS instance uses a non-default port for its engine", "CheckType": [ - "Software and Configuration Checks/AWS Security Best Practices" + "Software and Configuration Checks/AWS Security Best Practices/Network Reachability", + "TTPs/Discovery" ], "ServiceName": "rds", "SubServiceName": "", - "ResourceIdTemplate": "arn:aws:rds:region:account-id:db-instance", + "ResourceIdTemplate": "", "Severity": "low", "ResourceType": "AwsRdsDbInstance", "ResourceGroup": "database", - "Description": "Checks if an instance uses a port other than the default port of the database engine. The control fails if the RDS instance uses the default port.", - "Risk": "Using a default database port exposes the instance to potential security vulnerabilities, as attackers are more likely to target known, commonly-used ports. This may result in unauthorized access to the database or increased susceptibility to automated attacks.", - "RelatedUrl": "https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/Overview.DBInstance.Modifying.html", + "Description": "**RDS DB instances** are evaluated for use of a port that differs from the engine's default. Matching an engine with its default port-`3306` (MySQL/MariaDB/Aurora MySQL), `5432` (PostgreSQL/Aurora), `1521` (Oracle), `1433` (SQL Server), `50000` (Db2)-indicates the instance uses the default listener.", + "Risk": "Using a **default DB port** increases exposure to broad scans and eases **service fingerprinting**. With weak network controls, attackers can run **credential brute force**, target known **engine exploits**, or trigger **DoS** on the predictable port, threatening confidentiality and availability.", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://www.trendmicro.com/cloudoneconformity/knowledge-base/aws/RDS/rds-default-port.html", + "https://docs.aws.amazon.com/securityhub/latest/userguide/rds-controls.html#rds-23", + "https://docs.aws.amazon.com/cli/latest/reference/rds/modify-db-instance.html" + ], "Remediation": { "Code": { - "CLI": "aws rds modify-db-instance --db-instance-identifier --port ", - "NativeIaC": "", - "Other": "https://docs.aws.amazon.com/securityhub/latest/userguide/rds-controls.html#rds-23", - "Terraform": "" + "CLI": "aws rds modify-db-instance --db-instance-identifier --db-port ", + "NativeIaC": "```yaml\n# CloudFormation: set a non-default port on an RDS instance\nResources:\n :\n Type: AWS::RDS::DBInstance\n Properties:\n Port: # Critical: use a non-default DB engine port to pass the check\n```", + "Other": "1. In the AWS Console, go to Amazon RDS > Databases\n2. Select the DB instance and click Modify\n3. Set \"Database port\" to a non-default value for the engine (e.g., not 3306, 5432, 1521, 1433, or 50000)\n4. Click Continue, then Modify DB instance", + "Terraform": "```hcl\n# Terraform: set a non-default port on an RDS instance\nresource \"aws_db_instance\" \"\" {\n port = # Critical: use a non-default DB engine port to pass the check\n}\n```" }, "Recommendation": { - "Text": "Modify the RDS instance to use a non-default port, and ensure that the security group permits access to the new port.", - "Url": "https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/Overview.DBInstance.Modifying.html" + "Text": "Use a **non-default DB port** and enforce **defense in depth**:\n- Apply **least-privilege** network rules\n- Keep databases in **private subnets**; avoid public exposure\n- Require strong authentication and audit logging\n\n*Update client connection strings and security rules when the port changes.*", + "Url": "https://hub.prowler.com/check/rds_instance_non_default_port" } }, "Categories": [], diff --git a/prowler/providers/aws/services/rds/rds_instance_protected_by_backup_plan/rds_instance_protected_by_backup_plan.metadata.json b/prowler/providers/aws/services/rds/rds_instance_protected_by_backup_plan/rds_instance_protected_by_backup_plan.metadata.json index 3b6a01e899..79b4a2ede2 100644 --- a/prowler/providers/aws/services/rds/rds_instance_protected_by_backup_plan/rds_instance_protected_by_backup_plan.metadata.json +++ b/prowler/providers/aws/services/rds/rds_instance_protected_by_backup_plan/rds_instance_protected_by_backup_plan.metadata.json @@ -1,32 +1,40 @@ { "Provider": "aws", "CheckID": "rds_instance_protected_by_backup_plan", - "CheckTitle": "Check if RDS instances are protected by a backup plan.", + "CheckTitle": "RDS instance is protected by an AWS Backup plan", "CheckType": [ - "Software and Configuration Checks, AWS Security Best Practices" + "Software and Configuration Checks/AWS Security Best Practices", + "Software and Configuration Checks/Industry and Regulatory Standards/AWS Foundational Security Best Practices", + "Effects/Data Destruction" ], "ServiceName": "rds", "SubServiceName": "", - "ResourceIdTemplate": "arn:aws:rds:region:account-id:db-instance", - "Severity": "medium", + "ResourceIdTemplate": "", + "Severity": "high", "ResourceType": "AwsRdsDbInstance", "ResourceGroup": "database", - "Description": "Check if RDS instances are protected by a backup plan.", - "Risk": "Without a backup plan, RDS instances are vulnerable to data loss, accidental deletion, or corruption. This could lead to significant operational disruptions or loss of critical data.", - "RelatedUrl": "https://docs.aws.amazon.com/aws-backup/latest/devguide/assigning-resources.html", + "Description": "**RDS DB instances** (non-Aurora) are included in an **AWS Backup plan**, indicating scheduled backups and retention are applied to the resource.\n\n*Aurora engines are evaluated separately.*", + "Risk": "Without an **AWS Backup plan**, databases lack assured recovery, degrading **availability** and **integrity**. Likely outcomes: accidental deletion, corruption, or malicious wipes with no recent restore point. Expect missed `RPO/RTO`, extended outages, and data inconsistency after incidents.", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://docs.aws.amazon.com/aws-backup/latest/devguide/assigning-resources.html", + "https://docs.aws.amazon.com/securityhub/latest/userguide/rds-controls.html#rds-26" + ], "Remediation": { "Code": { - "CLI": "aws backup create-backup-plan --backup-plan , aws backup tag-resource --resource-arn --tags Key=backup,Value=true", - "NativeIaC": "", - "Other": "https://docs.aws.amazon.com/securityhub/latest/userguide/rds-controls.html#rds-26", - "Terraform": "" + "CLI": "aws backup create-backup-selection --backup-plan-id --backup-selection '{\"SelectionName\":\"\",\"IamRoleArn\":\"\",\"Resources\":[\"\"]}'", + "NativeIaC": "```yaml\n# CloudFormation: assign an RDS instance to an AWS Backup plan\nResources:\n :\n Type: AWS::Backup::BackupSelection\n Properties:\n BackupPlanId: # CRITICAL: targets the backup plan to protect the instance\n BackupSelection:\n SelectionName: \n IamRoleArn: # CRITICAL: role AWS Backup uses to back up the resource\n Resources:\n - # CRITICAL: assigns the RDS instance to the plan\n```", + "Other": "1. In the AWS Console, open AWS Backup\n2. Go to Settings > Service opt-in and enable Amazon RDS (if not already)\n3. Go to Backup plans and select an existing plan (or Create backup plan with defaults)\n4. Click Assign resources\n5. Enter a name, select an IAM role, and add the RDS instance (by ARN or resource picker)\n6. Click Assign resources to save", + "Terraform": "```hcl\n# Assign an RDS instance to an AWS Backup plan\nresource \"aws_backup_selection\" \"\" {\n name = \"\"\n plan_id = \"\" # CRITICAL: attaches to the backup plan\n iam_role_arn = \"\" # CRITICAL: role AWS Backup uses\n resources = [\"\"] # CRITICAL: RDS instance protected by the plan\n}\n```" }, "Recommendation": { - "Text": "Create a backup plan for the RDS instance to protect it from data loss, accidental deletion, or corruption.", - "Url": "https://docs.aws.amazon.com/aws-backup/latest/devguide/assigning-resources.html" + "Text": "Assign all non-Aurora RDS to an **AWS Backup plan** aligned to business `RPO/RTO`. Use **tags** for automatic coverage, define retention and lifecycle, and store backups in **immutable** vaults where possible. Regularly perform restore tests. Enforce **least privilege** and **separation of duties** for backup administration.", + "Url": "https://hub.prowler.com/check/rds_instance_protected_by_backup_plan" } }, - "Categories": [], + "Categories": [ + "resilience" + ], "DependsOn": [], "RelatedTo": [], "Notes": "" diff --git a/prowler/providers/aws/services/rds/rds_instance_storage_encrypted/rds_instance_storage_encrypted.metadata.json b/prowler/providers/aws/services/rds/rds_instance_storage_encrypted/rds_instance_storage_encrypted.metadata.json index 43d031fe1b..2125e64d1c 100644 --- a/prowler/providers/aws/services/rds/rds_instance_storage_encrypted/rds_instance_storage_encrypted.metadata.json +++ b/prowler/providers/aws/services/rds/rds_instance_storage_encrypted/rds_instance_storage_encrypted.metadata.json @@ -1,30 +1,47 @@ { "Provider": "aws", "CheckID": "rds_instance_storage_encrypted", - "CheckTitle": "Check if RDS instances storage is encrypted.", - "CheckType": [], + "CheckTitle": "RDS DB instance storage is encrypted at rest", + "CheckType": [ + "Software and Configuration Checks/AWS Security Best Practices", + "Software and Configuration Checks/Industry and Regulatory Standards/AWS Foundational Security Best Practices", + "Software and Configuration Checks/Industry and Regulatory Standards/NIST 800-53 Controls (USA)", + "Software and Configuration Checks/Industry and Regulatory Standards/NIST CSF Controls (USA)", + "Software and Configuration Checks/Industry and Regulatory Standards/PCI-DSS", + "Software and Configuration Checks/Industry and Regulatory Standards/ISO 27001 Controls", + "Software and Configuration Checks/Industry and Regulatory Standards/HIPAA Controls (USA)", + "Software and Configuration Checks/Industry and Regulatory Standards/GDPR Controls (Europe)", + "Effects/Data Exposure" + ], "ServiceName": "rds", "SubServiceName": "", - "ResourceIdTemplate": "arn:aws:rds:region:account-id:db-instance", - "Severity": "medium", + "ResourceIdTemplate": "", + "Severity": "high", "ResourceType": "AwsRdsDbInstance", "ResourceGroup": "database", - "Description": "Check if RDS instances storage is encrypted.", - "Risk": "If not enabled sensitive information at rest is not protected.", - "RelatedUrl": "https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/Overview.Encryption.html", + "Description": "**RDS DB instances** are assessed for **KMS-based encryption at rest** (`StorageEncrypted=true`), covering instance storage and derived artifacts such as snapshots, automated backups, and read replicas.", + "Risk": "Without **encryption at rest**, database files, snapshots, and automated backups remain in plaintext. An attacker with access to copied snapshots, compromised backups, or underlying storage can read sensitive data, causing loss of **confidentiality** and enabling large-scale exfiltration.", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://www.trendmicro.com/cloudoneconformity/knowledge-base/aws/RDS/rds-encryption-enabled.html", + "https://aws.amazon.com/blogs/storage/protecting-amazon-rds-db-instances-encrypted-using-kms-aws-managed-key-with-cross-account-and-cross-region-backups/", + "https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/Overview.Encryption.html" + ], "Remediation": { "Code": { - "CLI": "aws rds create-db-instance --db-instance-identifier --db-instance-class --engine --storage-encrypted true", - "NativeIaC": "https://docs.prowler.com/checks/aws/general-policies/general_4#cloudformation", - "Other": "https://www.trendmicro.com/cloudoneconformity/knowledge-base/aws/RDS/rds-encryption-enabled.html", - "Terraform": "https://docs.prowler.com/checks/aws/general-policies/general_4#terraform" + "CLI": "", + "NativeIaC": "```yaml\n# CloudFormation: create an encrypted RDS DB instance\nResources:\n :\n Type: AWS::RDS::DBInstance\n Properties:\n DBInstanceClass: \"\"\n Engine: \"\"\n AllocatedStorage: 20\n MasterUsername: \"\"\n MasterUserPassword: \"\"\n StorageEncrypted: true # CRITICAL: enables encryption at rest so the instance passes the check\n```", + "Other": "1. In the AWS Console, go to RDS > Databases, select the unencrypted DB instance, then choose Actions > Take snapshot.\n2. After the snapshot is available, go to Snapshots, select it, choose Actions > Copy snapshot, enable encryption, and select a KMS key (or aws/rds).\n3. When the encrypted copy is ready, select it and choose Actions > Restore snapshot to create a new (encrypted) DB instance.\n4. Update your application/endpoint to use the new encrypted DB instance.\n5. Decommission the old unencrypted instance after cutover.", + "Terraform": "```hcl\n# Terraform: create an encrypted RDS DB instance\nresource \"aws_db_instance\" \"\" {\n engine = \"\"\n instance_class = \"\"\n username = \"\"\n password = \"\"\n allocated_storage = 20\n\n storage_encrypted = true # CRITICAL: enables encryption at rest to pass the check\n}\n```" }, "Recommendation": { - "Text": "Enable Encryption. Use a CMK where possible. It will provide additional management and privacy benefits.", - "Url": "https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/Overview.Encryption.html" + "Text": "Enable **encryption at rest** for all RDS instances. Prefer **customer-managed KMS keys** to control rotation and fine-grained access, applying **least privilege** and **defense in depth**. Restrict key usage, monitor key activity, and manage key lifecycle. Migrate unencrypted instances via encrypted snapshot copy and restore.", + "Url": "https://hub.prowler.com/check/rds_instance_storage_encrypted" } }, - "Categories": [], + "Categories": [ + "encryption" + ], "DependsOn": [], "RelatedTo": [], "Notes": "" diff --git a/prowler/providers/aws/services/rds/rds_instance_transport_encrypted/rds_instance_transport_encrypted.metadata.json b/prowler/providers/aws/services/rds/rds_instance_transport_encrypted/rds_instance_transport_encrypted.metadata.json index 2180bc1b1c..38320956a2 100644 --- a/prowler/providers/aws/services/rds/rds_instance_transport_encrypted/rds_instance_transport_encrypted.metadata.json +++ b/prowler/providers/aws/services/rds/rds_instance_transport_encrypted/rds_instance_transport_encrypted.metadata.json @@ -1,27 +1,34 @@ { "Provider": "aws", "CheckID": "rds_instance_transport_encrypted", - "CheckTitle": "Check if RDS instances enforce SSL/TLS encryption for client connections (Microsoft SQL Server, PostgreSQL, MySQL, MariaDB, Aurora PostgreSQL, and Aurora MySQL).", - "CheckType": [], + "CheckTitle": "RDS instance or cluster enforces SSL/TLS encryption for client connections", + "CheckType": [ + "Software and Configuration Checks/AWS Security Best Practices/Network Security" + ], "ServiceName": "rds", "SubServiceName": "", - "ResourceIdTemplate": "arn:aws:rds:region:account-id:db-instance", + "ResourceIdTemplate": "", "Severity": "high", "ResourceType": "AwsRdsDbInstance", "ResourceGroup": "database", - "Description": "For SQL Server, PostgreSQL, and Aurora PostgreSQL databases, if the `rds.force_ssl` parameter value is set to 0, SSL/TLS connections are not enforced. For MySQL, Aurora MySQL, and MariaDB databases, if the `require_secure_transport` parameter value is set to OFF, SSL/TLS connections are not enforced. Enforcing SSL/TLS ensures that all client connections to RDS instances are encrypted, protecting sensitive information in transit.", - "Risk": "If not enabled, sensitive information in transit is not protected.", - "RelatedUrl": "https://aws.amazon.com/premiumsupport/knowledge-center/rds-connect-ssl-connection/", + "Description": "**RDS DB instances** and **DB clusters** enforce **SSL/TLS** for client connections via parameter groups. The check looks for `rds.force_ssl=1` (PostgreSQL, SQL Server) or `require_secure_transport` enabled (MySQL-family) and identifies databases where encryption enforcement isn't active.", + "Risk": "Without enforced **TLS**, clients can connect or downgrade to plaintext, exposing credentials and queries to interception. Adversaries can perform **MITM**, steal secrets, and tamper traffic, undermining **confidentiality** and **integrity** and enabling reuse of captured database credentials.", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://aws.amazon.com/premiumsupport/knowledge-center/rds-connect-ssl-connection/", + "https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/PostgreSQL.Concepts.General.SSL.html", + "https://www.trendmicro.com/cloudoneconformity/knowledge-base/aws/RDS/transport-encryption.html" + ], "Remediation": { "Code": { "CLI": "aws rds modify-db-parameter-group --region --db-parameter-group-name --parameters ParameterName='rds.force_ssl',ParameterValue='1',ApplyMethod='pending-reboot'", - "NativeIaC": "", - "Other": "https://www.trendmicro.com/cloudoneconformity/knowledge-base/aws/RDS/transport-encryption.html", - "Terraform": "" + "NativeIaC": "```yaml\n# CloudFormation: set required parameter to enforce SSL/TLS\nResources:\n ExampleDBParameterGroupPostgres:\n Type: AWS::RDS::DBParameterGroup\n Properties:\n Family: \n Description: Enforce SSL/TLS\n Parameters:\n rds.force_ssl: \"1\" # Critical: requires SSL/TLS for PostgreSQL/SQL Server instances\n\n ExampleDBParameterGroupMySQL:\n Type: AWS::RDS::DBParameterGroup\n Properties:\n Family: \n Description: Enforce SSL/TLS\n Parameters:\n require_secure_transport: \"1\" # Critical: requires SSL/TLS for MySQL/MariaDB instances\n\n ExampleDBClusterParameterGroupAuroraPostgres:\n Type: AWS::RDS::DBClusterParameterGroup\n Properties:\n Family: \n Description: Enforce SSL/TLS\n Parameters:\n rds.force_ssl: \"1\" # Critical: requires SSL/TLS for Aurora PostgreSQL clusters\n\n ExampleDBClusterParameterGroupAuroraMySQL:\n Type: AWS::RDS::DBClusterParameterGroup\n Properties:\n Family: \n Description: Enforce SSL/TLS\n Parameters:\n require_secure_transport: ON # Critical: requires SSL/TLS for Aurora MySQL clusters\n```", + "Other": "1. In the AWS Console, go to RDS > Parameter groups\n2. For DB instances:\n - Edit the DB parameter group attached to the instance (or create one and attach it)\n - Set rds.force_ssl = 1 for PostgreSQL/SQL Server, or require_secure_transport = 1 for MySQL/MariaDB\n - Save. If the parameter is static, reboot the instance\n3. For Aurora clusters:\n - Edit the DB cluster parameter group attached to the cluster (or create one and attach it)\n - Set rds.force_ssl = 1 for Aurora PostgreSQL, or require_secure_transport = ON for Aurora MySQL\n - Save. Reboot instances if changes are pending-reboot\n4. Verify the parameter group is associated to the target instance/cluster and status shows the new value applied", + "Terraform": "```hcl\n# DB instances\nresource \"aws_db_parameter_group\" \"example_pg\" {\n name = \"\"\n family = \"\"\n\n parameter {\n name = \"rds.force_ssl\"\n value = \"1\" # Critical: requires SSL/TLS for PostgreSQL/SQL Server instances\n }\n}\n\nresource \"aws_db_parameter_group\" \"example_mysql\" {\n name = \"\"\n family = \"\"\n\n parameter {\n name = \"require_secure_transport\"\n value = \"1\" # Critical: requires SSL/TLS for MySQL/MariaDB instances\n }\n}\n\n# Aurora clusters\nresource \"aws_rds_cluster_parameter_group\" \"example_aurora_pg\" {\n name = \"\"\n family = \"\"\n\n parameter {\n name = \"rds.force_ssl\"\n value = \"1\" # Critical: requires SSL/TLS for Aurora PostgreSQL clusters\n }\n}\n\nresource \"aws_rds_cluster_parameter_group\" \"example_aurora_mysql\" {\n name = \"\"\n family = \"\"\n\n parameter {\n name = \"require_secure_transport\"\n value = \"ON\" # Critical: requires SSL/TLS for Aurora MySQL clusters\n }\n}\n```" }, "Recommendation": { - "Text": "Ensure that instances provisioned with Amazon RDS enforce SSL/TLS for client connections to meet security and compliance requirements.", - "Url": "https://aws.amazon.com/premiumsupport/knowledge-center/rds-connect-ssl-connection/" + "Text": "Enforce transport encryption at the database layer:\n- Enable `rds.force_ssl=1` or `require_secure_transport` in parameter groups\n- Configure clients to require certificate validation and prevent fallback\n- Use current TLS versions and trusted CAs\n- Prefer private network access as **defense in depth**", + "Url": "https://hub.prowler.com/check/rds_instance_transport_encrypted" } }, "Categories": [ diff --git a/prowler/providers/aws/services/rds/rds_snapshots_encrypted/rds_snapshots_encrypted.metadata.json b/prowler/providers/aws/services/rds/rds_snapshots_encrypted/rds_snapshots_encrypted.metadata.json index 9693c841b0..5123981aff 100644 --- a/prowler/providers/aws/services/rds/rds_snapshots_encrypted/rds_snapshots_encrypted.metadata.json +++ b/prowler/providers/aws/services/rds/rds_snapshots_encrypted/rds_snapshots_encrypted.metadata.json @@ -1,27 +1,36 @@ { "Provider": "aws", "CheckID": "rds_snapshots_encrypted", - "CheckTitle": "Check if RDS Snapshots and Cluster Snapshots are encrypted.", - "CheckType": [], + "CheckTitle": "RDS DB instance snapshot or DB cluster snapshot is encrypted", + "CheckType": [ + "Software and Configuration Checks/AWS Security Best Practices", + "Software and Configuration Checks/Industry and Regulatory Standards/AWS Foundational Security Best Practices", + "Software and Configuration Checks/Industry and Regulatory Standards/CIS AWS Foundations Benchmark", + "Effects/Data Exposure" + ], "ServiceName": "rds", "SubServiceName": "", - "ResourceIdTemplate": "arn:aws:rds:region:account-id:snapshot", - "Severity": "medium", + "ResourceIdTemplate": "", + "Severity": "high", "ResourceType": "AwsRdsDbSnapshot", "ResourceGroup": "database", - "Description": "Check if RDS Snapshots and Cluster Snapshots are encrypted.", - "Risk": "Ensure that your manual Amazon RDS database snapshots are encrypted in order to achieve compliance for data-at-rest encryption within your organization.", - "RelatedUrl": "https://docs.aws.amazon.com/securityhub/latest/userguide/rds-controls.html#rds-4", + "Description": "**RDS DB snapshots** and **DB cluster snapshots** are evaluated for **encryption at rest**, identifying snapshots created with a KMS key versus unencrypted ones.", + "Risk": "Unencrypted snapshots enable direct access to full database data if backups are leaked, cross-account shared, or stolen. Adversaries can harvest data offline, bypassing network controls, leading to **loss of confidentiality**. Restores from such snapshots propagate the exposure to new instances.", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://www.trendmicro.com/cloudoneconformity/knowledge-base/aws/RDS/snapshot-encrypted.html#", + "https://docs.aws.amazon.com/securityhub/latest/userguide/rds-controls.html#rds-4" + ], "Remediation": { "Code": { - "CLI": "", - "NativeIaC": "https://www.trendmicro.com/cloudoneconformity/knowledge-base/aws/RDS/snapshot-encrypted.html#", - "Other": "https://www.trendmicro.com/cloudoneconformity/knowledge-base/aws/RDS/snapshot-encrypted.html#", - "Terraform": "https://www.trendmicro.com/cloudoneconformity/knowledge-base/aws/RDS/snapshot-encrypted.html#" + "CLI": "aws rds copy-db-snapshot --source-db-snapshot-identifier --target-db-snapshot-identifier -encrypted --kms-key-id ", + "NativeIaC": "", + "Other": "1. In the AWS Console, go to RDS > Snapshots\n2. Select the unencrypted snapshot (for clusters, use the DB cluster snapshots tab)\n3. Click Actions > Copy snapshot\n4. Check Enable encryption and choose a KMS key\n5. Click Copy snapshot and wait for completion\n6. After verifying the new encrypted snapshot, delete the original unencrypted snapshot (Actions > Delete snapshot)", + "Terraform": "```hcl\nresource \"aws_db_snapshot_copy\" \"\" {\n source_db_snapshot_identifier = \"\"\n target_db_snapshot_identifier = \"-encrypted\"\n kms_key_id = \"\" # Critical: encrypts the copied snapshot using the specified KMS key\n}\n```" }, "Recommendation": { - "Text": "When working with production databases that hold sensitive and critical data, it is strongly recommended to implement encryption at rest and protect your data from attackers or unauthorized personnel. ", - "Url": "https://docs.aws.amazon.com/securityhub/latest/userguide/rds-controls.html#rds-4" + "Text": "Encrypt all RDS snapshots at rest using **KMS**, preferably **customer-managed keys**. Apply **least privilege** to key usage, enforce encryption via templates and automation, and prevent sharing of unencrypted backups. Use **key rotation**, separation of duties, and ensure copies and cross-account shares remain encrypted.", + "Url": "https://hub.prowler.com/check/rds_snapshots_encrypted" } }, "Categories": [ diff --git a/prowler/providers/aws/services/rds/rds_snapshots_public_access/rds_snapshots_public_access.metadata.json b/prowler/providers/aws/services/rds/rds_snapshots_public_access/rds_snapshots_public_access.metadata.json index be565e27cb..19c0fbc838 100644 --- a/prowler/providers/aws/services/rds/rds_snapshots_public_access/rds_snapshots_public_access.metadata.json +++ b/prowler/providers/aws/services/rds/rds_snapshots_public_access/rds_snapshots_public_access.metadata.json @@ -1,27 +1,37 @@ { "Provider": "aws", "CheckID": "rds_snapshots_public_access", - "CheckTitle": "Check if RDS Snapshots and Cluster Snapshots are public.", - "CheckType": [], + "CheckTitle": "RDS snapshot is not publicly shared", + "CheckType": [ + "Software and Configuration Checks/AWS Security Best Practices", + "Software and Configuration Checks/Industry and Regulatory Standards/AWS Foundational Security Best Practices", + "Effects/Data Exposure", + "TTPs/Collection" + ], "ServiceName": "rds", "SubServiceName": "", - "ResourceIdTemplate": "arn:aws:rds:region:account-id:snapshot", + "ResourceIdTemplate": "", "Severity": "critical", "ResourceType": "AwsRdsDbSnapshot", "ResourceGroup": "database", - "Description": "Check if RDS Snapshots and Cluster Snapshots are public.", - "Risk": "Publicly accessible services could expose sensitive data to bad actors. t is recommended that your RDS snapshots should not be public in order to prevent potential leak or misuse of sensitive data or any other kind of security threat. If your RDS snapshot is public, then the data which is backed up in that snapshot is accessible to all other AWS accounts.", - "RelatedUrl": "https://docs.aws.amazon.com/config/latest/developerguide/rds-snapshots-public-prohibited.html", + "Description": "**RDS DB snapshots** and **DB cluster snapshots** with **public visibility** (shared with `all` AWS accounts) are detected.\n\nSnapshots limited to specific accounts or kept private are identified as restricted.", + "Risk": "Public RDS snapshots expose full database copies to all AWS accounts, risking:\n- Loss of confidentiality via data exfiltration (PII, secrets)\n- Offline cracking of hashes and schema reconnaissance\n- Credential harvesting from dumps enabling lateral movement\nThis directly compromises confidentiality and fuels targeted attacks.", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://www.trendmicro.com/cloudoneconformity/knowledge-base/aws/RDS/public-snapshots.html", + "https://docs.aws.amazon.com/config/latest/developerguide/rds-snapshots-public-prohibited.html", + "https://support.icompaas.com/support/solutions/articles/62000127056-ensure-rds-snapshots-and-cluster-snapshots-are-not-public" + ], "Remediation": { "Code": { "CLI": "aws rds modify-db-snapshot-attribute --db-snapshot-identifier --attribute-name restore --values-to-remove all", "NativeIaC": "", - "Other": "https://www.trendmicro.com/cloudoneconformity/knowledge-base/aws/RDS/public-snapshots.html", + "Other": "1. Open the Amazon RDS console and go to Snapshots\n2. Select the public snapshot (DB snapshot or DB cluster snapshot)\n3. Click Actions > Share snapshot\n4. Set visibility to Private (remove \"All\" from permissions) and click Save", "Terraform": "" }, "Recommendation": { - "Text": "Use AWS Config to identify any snapshot that is public.", - "Url": "https://docs.aws.amazon.com/config/latest/developerguide/rds-snapshots-public-prohibited.html" + "Text": "Keep **RDS snapshots** and **cluster snapshots** private. Share only with explicit AWS account IDs using **least privilege** and time-bound access.\n\nEnforce guardrails to block `public` visibility, require approvals for sharing, and audit snapshot permissions. Use encryption with strict key policies to control who can restore data.", + "Url": "https://hub.prowler.com/check/rds_snapshots_public_access" } }, "Categories": [ diff --git a/prowler/providers/aws/services/s3/s3_access_point_public_access_block/s3_access_point_public_access_block.metadata.json b/prowler/providers/aws/services/s3/s3_access_point_public_access_block/s3_access_point_public_access_block.metadata.json index 4863f43409..f22c40c8f1 100644 --- a/prowler/providers/aws/services/s3/s3_access_point_public_access_block/s3_access_point_public_access_block.metadata.json +++ b/prowler/providers/aws/services/s3/s3_access_point_public_access_block/s3_access_point_public_access_block.metadata.json @@ -1,32 +1,42 @@ { "Provider": "aws", "CheckID": "s3_access_point_public_access_block", - "CheckTitle": "Block Public Access Settings enabled on Access Points.", + "CheckTitle": "S3 access point has all Block Public Access settings enabled", "CheckType": [ - "Data Protection" + "Software and Configuration Checks/AWS Security Best Practices/Network Reachability", + "Software and Configuration Checks/Industry and Regulatory Standards/AWS Foundational Security Best Practices", + "Software and Configuration Checks/Industry and Regulatory Standards/CIS AWS Foundations Benchmark", + "Effects/Data Exposure" ], "ServiceName": "s3", "SubServiceName": "", - "ResourceIdTemplate": "arn:partition:s3:::bucket_name", + "ResourceIdTemplate": "", "Severity": "critical", "ResourceType": "AwsS3AccessPoint", "ResourceGroup": "storage", - "Description": "Ensures that public access is blocked on S3 Access Points.", - "Risk": "Leaving S3 access points open to the public in AWS can lead to data exposure, breaches, compliance violations, unauthorized access, and data integrity issues.", - "RelatedUrl": "https://docs.aws.amazon.com/AmazonS3/latest/userguide/access-points.html#access-points-policies", + "Description": "**Amazon S3 access points** have **Block Public Access** configured with all settings enabled: `block_public_acls`, `ignore_public_acls`, `block_public_policy`, and `restrict_public_buckets`.\n\nThe evaluation inspects each access point's public access block configuration.", + "Risk": "Without block public access on an access point, ACLs or policies can expose objects publicly despite intended restrictions. This enables unauthorized reads (**confidentiality** loss), writes or deletions (**integrity/availability** impact), and supports bulk data exfiltration or destructive actions.", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://docs.aws.amazon.com/config/latest/developerguide/s3-access-point-public-access-blocks.html", + "https://docs.aws.amazon.com/AmazonS3/latest/userguide/access-points.html#access-points-policies", + "https://docs.aws.amazon.com/securityhub/latest/userguide/s3-controls.html#s3-19" + ], "Remediation": { "Code": { "CLI": "", - "NativeIaC": "", - "Other": "https://docs.aws.amazon.com/securityhub/latest/userguide/s3-controls.html#s3-19", - "Terraform": "" + "NativeIaC": "```yaml\n# CloudFormation: S3 Access Point with all Block Public Access settings enabled\nResources:\n :\n Type: AWS::S3::AccessPoint\n Properties:\n Bucket: \n PublicAccessBlockConfiguration:\n BlockPublicAcls: true # Critical: block public ACLs\n IgnorePublicAcls: true # Critical: ignore any public ACLs\n BlockPublicPolicy: true # Critical: block public policies\n RestrictPublicBuckets: true # Critical: restrict public buckets\n```", + "Other": "1. In the AWS console, go to S3 > Access points\n2. Select the noncompliant access point and click Delete access point\n3. Click Create access point, select the same bucket\n4. Ensure Block public access is enabled (all options On by default)\n5. Click Create access point", + "Terraform": "```hcl\n# Terraform: S3 Access Point with all Block Public Access settings enabled\nresource \"aws_s3_access_point\" \"\" {\n name = \"\"\n bucket = \"\"\n\n public_access_block_configuration {\n block_public_acls = true # Critical: block public ACLs\n ignore_public_acls = true # Critical: ignore any public ACLs\n block_public_policy = true # Critical: block public policies\n restrict_public_buckets = true # Critical: restrict public buckets\n }\n}\n```" }, "Recommendation": { - "Text": "Ensure S3 access points are private by default, applying strict access controls, and regularly auditing permissions to prevent unauthorized public access.", - "Url": "https://docs.aws.amazon.com/config/latest/developerguide/s3-access-point-public-access-blocks.html" + "Text": "Enable all access-point Block Public Access settings (`block_public_acls`, `ignore_public_acls`, `block_public_policy`, `restrict_public_buckets`).\n\nApply **least privilege**, prefer **VPC-only** access points, and layer account and bucket blocks for **defense in depth**. Regularly audit for public principals like `Principal: *`.", + "Url": "https://hub.prowler.com/check/s3_access_point_public_access_block" } }, - "Categories": [], + "Categories": [ + "internet-exposed" + ], "DependsOn": [], "RelatedTo": [], "Notes": "" diff --git a/prowler/providers/aws/services/s3/s3_account_level_public_access_blocks/s3_account_level_public_access_blocks.metadata.json b/prowler/providers/aws/services/s3/s3_account_level_public_access_blocks/s3_account_level_public_access_blocks.metadata.json index a2caeca720..d9caa2a338 100644 --- a/prowler/providers/aws/services/s3/s3_account_level_public_access_blocks/s3_account_level_public_access_blocks.metadata.json +++ b/prowler/providers/aws/services/s3/s3_account_level_public_access_blocks/s3_account_level_public_access_blocks.metadata.json @@ -1,32 +1,40 @@ { "Provider": "aws", "CheckID": "s3_account_level_public_access_blocks", - "CheckTitle": "Check S3 Account Level Public Access Block.", + "CheckTitle": "S3 account-level Block Public Access ignores public ACLs and restricts public buckets", "CheckType": [ - "Data Protection" + "Software and Configuration Checks/AWS Security Best Practices", + "Software and Configuration Checks/Industry and Regulatory Standards/AWS Foundational Security Best Practices", + "Software and Configuration Checks/Industry and Regulatory Standards/CIS AWS Foundations Benchmark", + "Effects/Data Exposure" ], "ServiceName": "s3", "SubServiceName": "", - "ResourceIdTemplate": "arn:partition:s3:::bucket_name", + "ResourceIdTemplate": "", "Severity": "high", "ResourceType": "AwsS3AccountPublicAccessBlock", "ResourceGroup": "storage", - "Description": "Check S3 Account Level Public Access Block.", - "Risk": "Public access policies may be applied to sensitive data buckets.", + "Description": "**Amazon S3** account-level **Block Public Access** is assessed for `ignore_public_acls` and `restrict_public_buckets` to confirm centralized blocking of ACL-based public access and limiting buckets with public policies to in-account principals.", + "Risk": "Absent these settings, **public ACLs** and broad bucket policies may grant internet or cross-account access. This risks:\n- Confidentiality: bulk data exfiltration\n- Integrity: object overwrite/tampering\n- Availability: malicious deletions or malware hosting, triggering takedowns", "RelatedUrl": "", + "AdditionalURLs": [ + "https://docs.aws.amazon.com/AmazonS3/latest/userguide/access-control-block-public-access.html" + ], "Remediation": { "Code": { - "CLI": "aws s3control put-public-access-block --public-access-block-configuration BlockPublicAcls=true,IgnorePublicAcls=true,BlockPublicPolicy=true,RestrictPublicBuckets=true --account-id ", - "NativeIaC": "https://docs.prowler.com/checks/aws/s3-policies/bc_aws_s3_21#cloudformation", - "Other": "https://github.com/cloudmatos/matos/tree/master/remediations/aws/s3/s3control/block-public-access", - "Terraform": "https://docs.prowler.com/checks/aws/s3-policies/bc_aws_s3_21#terraform" + "CLI": "aws s3control put-public-access-block --account-id --public-access-block-configuration IgnorePublicAcls=true,RestrictPublicBuckets=true", + "NativeIaC": "```yaml\n# CloudFormation: Enable required S3 Account-level Block Public Access settings\nResources:\n :\n Type: AWS::S3::AccountPublicAccessBlock\n Properties:\n AccountId: !Ref AWS::AccountId\n IgnorePublicAcls: true # Critical: Ignores any public ACLs at the account level\n RestrictPublicBuckets: true # Critical: Restricts buckets with public policies to only same-account principals\n```", + "Other": "1. In the AWS Console, go to S3\n2. Click Block public access (account settings)\n3. Click Edit\n4. Turn on: Ignore public ACLs and Restrict public buckets\n5. Click Save changes", + "Terraform": "```hcl\n# Terraform: Enable required S3 Account-level Block Public Access settings\nresource \"aws_s3_account_public_access_block\" \"\" {\n ignore_public_acls = true # Critical: Ignores any public ACLs account-wide\n restrict_public_buckets = true # Critical: Restricts buckets with public policies to same-account principals\n}\n```" }, "Recommendation": { - "Text": "You can enable Public Access Block at the account level to prevent the exposure of your data stored in S3.", - "Url": "https://docs.aws.amazon.com/AmazonS3/latest/userguide/access-control-block-public-access.html" + "Text": "Turn on account-level **Block Public Access** (prefer enabling all four: `block_public_acls`, `ignore_public_acls`, `block_public_policy`, `restrict_public_buckets`) to enforce least privilege. For legitimate access, use private buckets with **CloudFront**, VPC endpoints, or presigned URLs. Regularly review policies with IAM Access Analyzer.", + "Url": "https://hub.prowler.com/check/s3_account_level_public_access_blocks" } }, - "Categories": [], + "Categories": [ + "internet-exposed" + ], "DependsOn": [], "RelatedTo": [], "Notes": "" diff --git a/prowler/providers/aws/services/s3/s3_bucket_acl_prohibited/s3_bucket_acl_prohibited.metadata.json b/prowler/providers/aws/services/s3/s3_bucket_acl_prohibited/s3_bucket_acl_prohibited.metadata.json index b33e90e09e..3d20a8d11d 100644 --- a/prowler/providers/aws/services/s3/s3_bucket_acl_prohibited/s3_bucket_acl_prohibited.metadata.json +++ b/prowler/providers/aws/services/s3/s3_bucket_acl_prohibited/s3_bucket_acl_prohibited.metadata.json @@ -1,32 +1,40 @@ { "Provider": "aws", "CheckID": "s3_bucket_acl_prohibited", - "CheckTitle": "Check if S3 buckets have ACLs enabled", + "CheckTitle": "S3 bucket has bucket ACLs disabled", "CheckType": [ - "Logging and Monitoring" + "Software and Configuration Checks/AWS Security Best Practices", + "Software and Configuration Checks/Industry and Regulatory Standards/AWS Foundational Security Best Practices", + "Software and Configuration Checks/Industry and Regulatory Standards/CIS AWS Foundations Benchmark" ], "ServiceName": "s3", "SubServiceName": "", - "ResourceIdTemplate": "arn:partition:s3:::bucket_name", + "ResourceIdTemplate": "", "Severity": "medium", "ResourceType": "AwsS3Bucket", "ResourceGroup": "storage", - "Description": "Check if S3 buckets have ACLs enabled", - "Risk": "S3 ACLs are a legacy access control mechanism that predates IAM. IAM and bucket policies are currently the preferred methods.", + "Description": "**Amazon S3 buckets** are evaluated for **Object Ownership** set to `BucketOwnerEnforced`, which disables bucket and object ACLs. Buckets using any other ownership setting indicate that ACLs remain enabled.", + "Risk": "With **ACLs enabled**, access can bypass centralized policy controls, impacting confidentiality and integrity.\n- Unintended public or cross-account reads/writes\n- Object-writer ownership blocking bucket-owner governance\n- Per-object grants hinder auditing, enabling data exfiltration or tampering", "RelatedUrl": "", + "AdditionalURLs": [ + "https://docs.aws.amazon.com/AmazonS3/latest/userguide/about-object-ownership.html", + "https://docs.aws.amazon.com/AmazonS3/latest/userguide/ensure-object-ownership.html" + ], "Remediation": { "Code": { - "CLI": "aws s3api put-bucket-ownership-controls --bucket --ownership-controls Rules=[{ObjectOwnership=BucketOwnerEnforced}]", - "NativeIaC": "https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-s3-bucket-ownershipcontrols.html", - "Other": "", - "Terraform": "" + "CLI": "aws s3api put-bucket-ownership-controls --bucket --ownership-controls \"Rules=[{ObjectOwnership=BucketOwnerEnforced}]\"", + "NativeIaC": "```yaml\n# CloudFormation: Disable ACLs by enforcing bucket owner for all objects\nResources:\n OwnershipControls:\n Type: AWS::S3::BucketOwnershipControls\n Properties:\n Bucket: \n OwnershipControls:\n Rules:\n - ObjectOwnership: BucketOwnerEnforced # Critical: Disables ACLs and makes bucket owner the object owner\n```", + "Other": "1. In the AWS Console, go to S3 > Buckets and select the target bucket\n2. Open the Permissions tab\n3. In Object Ownership, click Edit\n4. Select Bucket owner enforced (ACLs disabled)\n5. Click Save changes", + "Terraform": "```hcl\n# Disable ACLs by enforcing bucket owner for all objects\nresource \"aws_s3_bucket_ownership_controls\" \"\" {\n bucket = \"\"\n rule {\n object_ownership = \"BucketOwnerEnforced\" # Critical: Disables ACLs and enforces bucket owner\n }\n}\n```" }, "Recommendation": { - "Text": "Ensure that S3 ACLs are disabled (BucketOwnerEnforced). Use IAM policies and bucket policies to manage access.", - "Url": "https://docs.aws.amazon.com/AmazonS3/latest/userguide/about-object-ownership.html" + "Text": "Disable ACLs by setting **Object Ownership** to `BucketOwnerEnforced` and manage access with **IAM** and **bucket policies** under **least privilege**. Centralize authorization, review policies regularly, and use organizational guardrails to prevent re-enabling ACLs. *Migrate ACL-based grants into policies before the change.*", + "Url": "https://hub.prowler.com/check/s3_bucket_acl_prohibited" } }, - "Categories": [], + "Categories": [ + "identity-access" + ], "DependsOn": [], "RelatedTo": [], "Notes": "" diff --git a/prowler/providers/aws/services/s3/s3_bucket_cross_account_access/s3_bucket_cross_account_access.metadata.json b/prowler/providers/aws/services/s3/s3_bucket_cross_account_access/s3_bucket_cross_account_access.metadata.json index c71b56abdf..c92912b2cd 100644 --- a/prowler/providers/aws/services/s3/s3_bucket_cross_account_access/s3_bucket_cross_account_access.metadata.json +++ b/prowler/providers/aws/services/s3/s3_bucket_cross_account_access/s3_bucket_cross_account_access.metadata.json @@ -1,32 +1,42 @@ { "Provider": "aws", "CheckID": "s3_bucket_cross_account_access", - "CheckTitle": "Ensure that general-purpose bucket policies restrict access to other AWS accounts.", + "CheckTitle": "S3 bucket policy does not allow cross-account access", "CheckType": [ + "Software and Configuration Checks/AWS Security Best Practices", + "Software and Configuration Checks/Industry and Regulatory Standards/AWS Foundational Security Best Practices", + "TTPs/Initial Access/Unauthorized Access", "Effects/Data Exposure" ], "ServiceName": "s3", "SubServiceName": "", - "ResourceIdTemplate": "arn:partition:s3:::bucket_name", + "ResourceIdTemplate": "", "Severity": "high", "ResourceType": "AwsS3Bucket", "ResourceGroup": "storage", - "Description": "This check verifies that S3 bucket policies are configured in a way that limits access to the intended AWS accounts only, preventing unauthorized access by external or unintended accounts.", - "Risk": "Allowing other AWS accounts to perform sensitive actions (e.g., modifying bucket policies, ACLs, or encryption settings) on your S3 buckets can lead to data exposure, unauthorized access, or misconfigurations, increasing the risk of insider threats or attacks.", - "RelatedUrl": "https://docs.aws.amazon.com/AmazonS3/latest/userguide/access-control-block-public-access.html", + "Description": "**Amazon S3 bucket policies** are analyzed for statements that grant **cross-account access**.\n\nAny policy that names principals outside the owning account (other account IDs or `Principal: \"*\"`) is treated as cross-account; absence of a policy implies no cross-account grants.", + "Risk": "Cross-account grants can let external principals read, write, or administer the bucket, impacting:\n- Confidentiality: unauthorized object access/exfiltration\n- Integrity: object tampering, policy or encryption changes\n- Availability: deletions, versioning changes, or lockouts causing data loss and downtime", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://docs.aws.amazon.com/securityhub/latest/userguide/s3-controls.html#s3-6", + "https://docs.aws.amazon.com/AmazonS3/latest/userguide/access-control-block-public-access.html" + ], "Remediation": { "Code": { - "CLI": "", - "NativeIaC": "", - "Other": "https://docs.aws.amazon.com/securityhub/latest/userguide/s3-controls.html#s3-6", - "Terraform": "" + "CLI": "aws s3api delete-bucket-policy --bucket ", + "NativeIaC": "```yaml\n# CloudFormation: restrict bucket policy to this AWS account only\nResources:\n BucketPolicy:\n Type: AWS::S3::BucketPolicy\n Properties:\n Bucket: \n PolicyDocument:\n Version: '2012-10-17'\n Statement:\n - Effect: Allow\n Principal:\n AWS: !Sub arn:aws:iam::${AWS::AccountId}:root # Critical: limits access to the bucket owner's account, preventing cross-account access\n Action: s3:*\n Resource:\n - !Sub arn:aws:s3:::\n - !Sub arn:aws:s3:::/*\n```", + "Other": "1. In the AWS console, go to S3 > Buckets > select the bucket\n2. Open the Permissions tab > Bucket policy\n3. Click Delete bucket policy (or remove all statements) and Save\n4. If a policy is required, ensure all statements only use your account as Principal (arn:aws:iam:::root); remove any other accounts or \"*\"", + "Terraform": "```hcl\n# Terraform: restrict bucket policy to this AWS account only\nresource \"aws_s3_bucket_policy\" \"\" {\n bucket = \"\"\n policy = jsonencode({\n Version = \"2012-10-17\"\n Statement = [{\n Effect = \"Allow\"\n Principal = { AWS = \"arn:aws:iam:::root\" } # Critical: limits access to the bucket owner's account, preventing cross-account access\n Action = \"s3:*\"\n Resource = [\n \"arn:aws:s3:::\",\n \"arn:aws:s3:::/*\"\n ]\n }]\n })\n}\n```" }, "Recommendation": { - "Text": "Review and update your S3 bucket policies to remove permissions that grant external AWS accounts access to critical actions and implement least privilege principles to ensure sensitive operations are restricted to trusted accounts only", - "Url": "https://docs.aws.amazon.com/AmazonS3/latest/userguide/access-control-block-public-access.html" + "Text": "Enforce **least privilege**: limit bucket policy `Principal` to your account or approved org IDs with fixed values; avoid wildcards.\n\nUse **role-based cross-account access** with scoped permissions when needed. Add **defense-in-depth** conditions (private networks, TLS), and periodically review for unintended external access.", + "Url": "https://hub.prowler.com/check/s3_bucket_cross_account_access" } }, - "Categories": [], + "Categories": [ + "identity-access", + "trust-boundaries" + ], "DependsOn": [], "RelatedTo": [], "Notes": "" diff --git a/prowler/providers/aws/services/s3/s3_bucket_cross_region_replication/s3_bucket_cross_region_replication.metadata.json b/prowler/providers/aws/services/s3/s3_bucket_cross_region_replication/s3_bucket_cross_region_replication.metadata.json index a5cf1642b8..932ba8ed9c 100644 --- a/prowler/providers/aws/services/s3/s3_bucket_cross_region_replication/s3_bucket_cross_region_replication.metadata.json +++ b/prowler/providers/aws/services/s3/s3_bucket_cross_region_replication/s3_bucket_cross_region_replication.metadata.json @@ -1,33 +1,38 @@ { "Provider": "aws", "CheckID": "s3_bucket_cross_region_replication", - "CheckTitle": "Check if S3 buckets use cross region replication.", + "CheckTitle": "S3 bucket has cross-region replication configured to a bucket in a different region", "CheckType": [ - "Secure access management" + "Software and Configuration Checks/AWS Security Best Practices", + "Effects/Data Destruction" ], "ServiceName": "s3", "SubServiceName": "", - "ResourceIdTemplate": "arn:partition:s3:::bucket_name", + "ResourceIdTemplate": "", "Severity": "low", "ResourceType": "AwsS3Bucket", "ResourceGroup": "storage", - "Description": "Verifying whether S3 buckets have cross-region replication enabled, ensuring data redundancy and availability across multiple AWS regions", - "Risk": "Without cross-region replication in S3 buckets, data is at risk of being lost or inaccessible if an entire region goes down, leading to potential service disruptions and data unavailability.", - "RelatedUrl": "https://docs.aws.amazon.com/AmazonS3/latest/userguide/replication.html", + "Description": "**Amazon S3 buckets** use **cross-Region replication** with `versioning` and an enabled rule that targets a destination bucket in a different AWS Region.\n\nBuckets with same-Region targets, missing destinations, or disabled `versioning` don't meet this replication posture.", + "Risk": "**Single-Region storage** creates an availability gap: a Regional outage, control-plane isolation, or denial of service can make data **unreachable**.\n\nLack of replication raises RPO/RTO, delaying recovery and disrupting multi-Region workloads. Missing replicas also weaken data **integrity** during restore from corruption or deletion.", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://docs.aws.amazon.com/securityhub/latest/userguide/s3-controls.html#s3-7", + "https://docs.aws.amazon.com/AmazonS3/latest/userguide/replication.html" + ], "Remediation": { "Code": { "CLI": "", - "NativeIaC": "", - "Other": "https://docs.aws.amazon.com/securityhub/latest/userguide/s3-controls.html#s3-7", - "Terraform": "https://docs.prowler.com/checks/aws/general-policies/ensure-that-s3-bucket-has-cross-region-replication-enabled#terraform" + "NativeIaC": "```yaml\n# CloudFormation: Enable versioning and CRR to a bucket in another region\nResources:\n ExampleBucket:\n Type: AWS::S3::Bucket\n Properties:\n VersioningConfiguration:\n Status: Enabled # critical: enables versioning (required for replication)\n ReplicationConfiguration: # critical: enables cross-Region replication\n Role: !GetAtt ReplicationRole.Arn\n Rules:\n - Status: Enabled # critical: replication rule must be enabled\n Destination:\n Bucket: arn:aws:s3::: # critical: destination bucket ARN in a different region\n\n ReplicationRole:\n Type: AWS::IAM::Role\n Properties:\n AssumeRolePolicyDocument:\n Version: '2012-10-17'\n Statement:\n - Effect: Allow\n Principal:\n Service: s3.amazonaws.com\n Action: sts:AssumeRole\n Policies:\n - PolicyName: s3-replication-minimal\n PolicyDocument:\n Version: '2012-10-17'\n Statement:\n - Effect: Allow\n Action:\n - s3:ListBucket\n - s3:GetReplicationConfiguration\n Resource: !Sub arn:aws:s3:::${ExampleBucket}\n - Effect: Allow\n Action:\n - s3:GetObjectVersionForReplication\n - s3:GetObjectVersionAcl\n - s3:GetObjectVersionTagging\n Resource: !Sub arn:aws:s3:::${ExampleBucket}/*\n - Effect: Allow\n Action:\n - s3:ReplicateObject\n - s3:ReplicateDelete\n - s3:ReplicateTags\n Resource: arn:aws:s3:::/*\n```", + "Other": "1. In the S3 console, open the source bucket\n2. Go to Properties > Bucket Versioning and click Enable\n3. Go to Management > Replication rules > Create replication rule\n4. Scope: Apply to all objects in the bucket\n5. Destination: Select a bucket in a different AWS Region\n6. Status: Ensure Enabled is selected\n7. IAM role: Choose Create new role (recommended)\n8. Save the rule", + "Terraform": "```hcl\n# Enable versioning and minimal CRR to a bucket in another region\nresource \"aws_s3_bucket\" \"source\" {\n bucket = \"\"\n\n versioning { \n enabled = true # critical: required for replication\n }\n\n replication_configuration { \n role = aws_iam_role.replication.arn # critical: role used by S3 to replicate\n\n rules {\n status = \"Enabled\" # critical: replication rule must be enabled\n destination {\n bucket = \"arn:aws:s3:::\" # critical: destination bucket ARN in a different region\n }\n }\n }\n}\n\nresource \"aws_iam_role\" \"replication\" {\n name = \"-s3-replication-role\"\n assume_role_policy = jsonencode({\n Version = \"2012-10-17\"\n Statement = [{\n Effect = \"Allow\"\n Principal = { Service = \"s3.amazonaws.com\" }\n Action = \"sts:AssumeRole\"\n }]\n })\n}\n\nresource \"aws_iam_role_policy\" \"replication\" {\n name = \"-s3-replication-policy\"\n role = aws_iam_role.replication.id\n policy = jsonencode({\n Version = \"2012-10-17\",\n Statement = [\n {\n Effect = \"Allow\",\n Action = [\"s3:ListBucket\", \"s3:GetReplicationConfiguration\"],\n Resource = \"arn:aws:s3:::${aws_s3_bucket.source.bucket}\"\n },\n {\n Effect = \"Allow\",\n Action = [\n \"s3:GetObjectVersionForReplication\",\n \"s3:GetObjectVersionAcl\",\n \"s3:GetObjectVersionTagging\"\n ],\n Resource = \"arn:aws:s3:::${aws_s3_bucket.source.bucket}/*\"\n },\n {\n Effect = \"Allow\",\n Action = [\"s3:ReplicateObject\", \"s3:ReplicateDelete\", \"s3:ReplicateTags\"],\n Resource = \"arn:aws:s3:::/*\"\n }\n ]\n })\n}\n```" }, "Recommendation": { - "Text": "Ensure that S3 buckets have cross region replication.", - "Url": "https://docs.aws.amazon.com/AmazonS3/latest/userguide/replication-walkthrough1.html" + "Text": "Enable **CRR** to a different Region with `versioning` and least-privilege roles.\n\n- Replicate needed prefixes and metadata\n- Consider `S3 Replication Time Control` for tighter RPO\n- Protect deletes via `delete marker` strategy and Object Lock\n- Monitor replication metrics and test DR regularly\n\nAlign with **defense in depth** and availability by design.", + "Url": "https://hub.prowler.com/check/s3_bucket_cross_region_replication" } }, "Categories": [ - "redundancy" + "resilience" ], "DependsOn": [], "RelatedTo": [], diff --git a/prowler/providers/aws/services/s3/s3_bucket_default_encryption/s3_bucket_default_encryption.metadata.json b/prowler/providers/aws/services/s3/s3_bucket_default_encryption/s3_bucket_default_encryption.metadata.json index 569aa01dcc..eb0c07e8af 100644 --- a/prowler/providers/aws/services/s3/s3_bucket_default_encryption/s3_bucket_default_encryption.metadata.json +++ b/prowler/providers/aws/services/s3/s3_bucket_default_encryption/s3_bucket_default_encryption.metadata.json @@ -1,29 +1,37 @@ { "Provider": "aws", "CheckID": "s3_bucket_default_encryption", - "CheckTitle": "Check if S3 buckets have default encryption (SSE) enabled or use a bucket policy to enforce it.", + "CheckTitle": "S3 bucket has default server-side encryption (SSE) enabled", "CheckType": [ - "Data Protection" + "Software and Configuration Checks/AWS Security Best Practices", + "Software and Configuration Checks/Industry and Regulatory Standards/AWS Foundational Security Best Practices", + "Software and Configuration Checks/Industry and Regulatory Standards/CIS AWS Foundations Benchmark", + "Effects/Data Exposure" ], "ServiceName": "s3", "SubServiceName": "", - "ResourceIdTemplate": "arn:partition:s3:::bucket_name", + "ResourceIdTemplate": "", "Severity": "medium", "ResourceType": "AwsS3Bucket", "ResourceGroup": "storage", - "Description": "Check if S3 buckets have default encryption (SSE) enabled or use a bucket policy to enforce it.", - "Risk": "Amazon S3 default encryption provides a way to set the default encryption behavior for an S3 bucket. This will ensure data-at-rest is encrypted.", + "Description": "**Amazon S3 buckets** have a default **server-side encryption** setting that automatically encrypts new objects using `SSE-S3` or `SSE-KMS`. This evaluates whether a bucket has a default encryption configuration defined.", + "Risk": "Without default encryption, older objects may remain unencrypted and new uploads won't be forced to use `SSE-KMS`. This reduces confidentiality and governance by limiting key audit logs, rotation, and cross-account controls, and increases exposure if data is copied, replicated, or accessed outside intended paths.", "RelatedUrl": "", + "AdditionalURLs": [ + "https://docs.amazonaws.cn/en_us/AmazonS3/latest/userguide/bucket-encryption.html", + "https://aws.amazon.com/blogs/security/how-to-prevent-uploads-of-unencrypted-objects-to-amazon-s3/", + "https://docs.aws.amazon.com/us_en/AmazonS3/latest/userguide/default-encryption-faq.html" + ], "Remediation": { "Code": { - "CLI": "aws s3api put-bucket-encryption --bucket --server-side-encryption-configuration '{'Rules': [{'ApplyServerSideEncryptionByDefault': {'SSEAlgorithm': 'AES256'}}]}'", - "NativeIaC": "https://docs.prowler.com/checks/aws/s3-policies/s3_14-data-encrypted-at-rest#cloudformation", - "Other": "", - "Terraform": "https://docs.prowler.com/checks/aws/s3-policies/s3_14-data-encrypted-at-rest#terraform" + "CLI": "aws s3api put-bucket-encryption --bucket --server-side-encryption-configuration '{\"Rules\":[{\"ApplyServerSideEncryptionByDefault\":{\"SSEAlgorithm\":\"AES256\"}}]}'", + "NativeIaC": "```yaml\nResources:\n :\n Type: AWS::S3::Bucket\n Properties:\n BucketEncryption:\n ServerSideEncryptionConfiguration:\n - ServerSideEncryptionByDefault:\n SSEAlgorithm: AES256 # Critical: enables default SSE-S3 so new objects are encrypted\n```", + "Other": "1. Open the AWS S3 console and select the bucket\n2. Go to the Properties tab\n3. In Default encryption, click Edit\n4. Enable default encryption and select SSE-S3 (AES-256)\n5. Click Save changes", + "Terraform": "```hcl\nresource \"aws_s3_bucket\" \"\" {\n bucket = \"\"\n}\n\nresource \"aws_s3_bucket_server_side_encryption_configuration\" \"\" {\n bucket = aws_s3_bucket..id\n\n rule {\n apply_server_side_encryption_by_default {\n sse_algorithm = \"AES256\" # Critical: enables default SSE-S3 for the bucket\n }\n }\n}\n```" }, "Recommendation": { - "Text": "Ensure that S3 buckets have encryption at rest enabled.", - "Url": "https://aws.amazon.com/blogs/security/how-to-prevent-uploads-of-unencrypted-objects-to-amazon-s3/" + "Text": "Enable default encryption on all buckets, preferring `SSE-KMS` for sensitive data to retain key control and auditing. Enforce encryption with restrictive bucket policies, apply **least privilege** to KMS keys with rotation, and re-encrypt existing objects. Use **defense in depth** monitoring to detect drift and noncompliant uploads.", + "Url": "https://hub.prowler.com/check/s3_bucket_default_encryption" } }, "Categories": [ diff --git a/prowler/providers/aws/services/s3/s3_bucket_event_notifications_enabled/s3_bucket_event_notifications_enabled.metadata.json b/prowler/providers/aws/services/s3/s3_bucket_event_notifications_enabled/s3_bucket_event_notifications_enabled.metadata.json index 5d67f3325b..eecbe4df3e 100644 --- a/prowler/providers/aws/services/s3/s3_bucket_event_notifications_enabled/s3_bucket_event_notifications_enabled.metadata.json +++ b/prowler/providers/aws/services/s3/s3_bucket_event_notifications_enabled/s3_bucket_event_notifications_enabled.metadata.json @@ -1,32 +1,40 @@ { "Provider": "aws", "CheckID": "s3_bucket_event_notifications_enabled", - "CheckTitle": "Check if S3 buckets have event notifications enabled.", + "CheckTitle": "S3 bucket has event notifications enabled", "CheckType": [ + "Software and Configuration Checks/AWS Security Best Practices", "Software and Configuration Checks/Industry and Regulatory Standards/NIST 800-53 Controls" ], "ServiceName": "s3", "SubServiceName": "", - "ResourceIdTemplate": "arn:partition:s3:::bucket_name", - "Severity": "medium", + "ResourceIdTemplate": "", + "Severity": "low", "ResourceType": "AwsS3Bucket", "ResourceGroup": "storage", - "Description": "Ensure whether S3 buckets have event notifications enabled.", - "Risk": "Without event notifications, important actions on S3 buckets may go unnoticed, leading to missed opportunities for timely response to critical changes, such as object creation, deletion, or updates that could impact data security and availability.", - "RelatedUrl": "https://docs.aws.amazon.com/AmazonS3/latest/userguide/notification-how-to-event-types-and-destinations.html#supported-notification-event-types", + "Description": "**Amazon S3 buckets** define a **notification configuration** that publishes bucket events (for example `s3:ObjectCreated:*`, `s3:ObjectRemoved:*`) to a destination. The evaluation identifies buckets that lack any notification setup.", + "Risk": "Missing notifications leaves object and bucket changes **unseen**, weakening **integrity** and **availability** oversight. Undetected deletions, policy drift, or replication issues can stall data pipelines (S3 to Lambda/SQS), slow incident response, and allow tampering or exfiltration to persist longer.", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://docs.aws.amazon.com/securityhub/latest/userguide/s3-controls.html#s3-11", + "https://docs.aws.amazon.com/AmazonS3/latest/userguide/enable-event-notifications.html", + "https://docs.aws.amazon.com/AmazonS3/latest/userguide/EventNotifications.html" + ], "Remediation": { "Code": { - "CLI": "", - "NativeIaC": "", - "Other": "https://docs.aws.amazon.com/securityhub/latest/userguide/s3-controls.html#s3-11", - "Terraform": "" + "CLI": "aws s3api put-bucket-notification-configuration --bucket --notification-configuration '{\"EventBridgeConfiguration\": {}}'", + "NativeIaC": "```yaml\n# CloudFormation: Enable S3 event notifications via EventBridge\nResources:\n :\n Type: AWS::S3::Bucket\n Properties:\n NotificationConfiguration:\n EventBridgeConfiguration: {} # Critical: turns on EventBridge notifications, making notifications enabled\n```", + "Other": "1. Open the S3 console and select your bucket\n2. Go to the Properties tab\n3. In Event notifications, find Amazon EventBridge and turn it On (Enable)\n4. Click Save changes", + "Terraform": "```hcl\n# Enable S3 event notifications via EventBridge\nresource \"aws_s3_bucket_notification\" \"\" {\n bucket = \"\"\n\n eventbridge {} # Critical: enables EventBridge delivery, satisfying notifications enabled\n}\n```" }, "Recommendation": { - "Text": "Enable event notifications for all S3 general-purpose buckets to monitor important events such as object creation, deletion, tagging, and lifecycle events, ensuring visibility and quick action on relevant changes.", - "Url": "https://docs.aws.amazon.com/AmazonS3/latest/userguide/EventNotifications.html" + "Text": "Enable **S3 event notifications** for relevant events (e.g., `s3:ObjectCreated:*`, `s3:ObjectRemoved:*`) and route to controlled destinations (SNS, SQS, Lambda, EventBridge).\n\nUse prefix/suffix filters, avoid recursive triggers, and enforce **least privilege** on targets. Pair with object-level logging for **defense in depth**.", + "Url": "https://hub.prowler.com/check/s3_bucket_event_notifications_enabled" } }, - "Categories": [], + "Categories": [ + "logging" + ], "DependsOn": [], "RelatedTo": [], "Notes": "" diff --git a/prowler/providers/aws/services/s3/s3_bucket_kms_encryption/s3_bucket_kms_encryption.metadata.json b/prowler/providers/aws/services/s3/s3_bucket_kms_encryption/s3_bucket_kms_encryption.metadata.json index d8741fc837..9c593ab704 100644 --- a/prowler/providers/aws/services/s3/s3_bucket_kms_encryption/s3_bucket_kms_encryption.metadata.json +++ b/prowler/providers/aws/services/s3/s3_bucket_kms_encryption/s3_bucket_kms_encryption.metadata.json @@ -1,29 +1,37 @@ { "Provider": "aws", "CheckID": "s3_bucket_kms_encryption", - "CheckTitle": "Check if S3 buckets have KMS encryption enabled.", + "CheckTitle": "S3 bucket has server-side encryption with AWS KMS", "CheckType": [ - "Data Protection" + "Software and Configuration Checks/AWS Security Best Practices", + "Software and Configuration Checks/Industry and Regulatory Standards/AWS Foundational Security Best Practices", + "Software and Configuration Checks/Industry and Regulatory Standards/CIS AWS Foundations Benchmark", + "Effects/Data Exposure" ], "ServiceName": "s3", "SubServiceName": "", - "ResourceIdTemplate": "arn:partition:s3:::bucket_name", + "ResourceIdTemplate": "", "Severity": "medium", "ResourceType": "AwsS3Bucket", "ResourceGroup": "storage", - "Description": "Check if S3 buckets have KMS encryption enabled.", - "Risk": "Amazon S3 KMS encryption provides a way to set the encryption behavior for an S3 bucket using a managed key. This will ensure data-at-rest is encrypted.", - "RelatedUrl": "https://docs.aws.amazon.com/AmazonS3/latest/userguide/UsingKMSEncryption.html", + "Description": "**Amazon S3 buckets** use server-side encryption with **AWS KMS** keys, including dual-layer `aws:kms:dsse`. The evaluation identifies buckets whose default encryption is `aws:kms` or `aws:kms:dsse` rather than SSE-S3.", + "Risk": "Without **KMS-based encryption**, data relies only on SSE-S3, reducing **confidentiality** controls. Missing key policies and grants weakens **least privilege**, cross-account scoping, and the ability to disable or rotate keys. Lack of **KMS audit trails** obscures key usage, hindering detection of misuse and **defense in depth**.", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://docs.aws.amazon.com/AmazonS3/latest/userguide/default-bucket-encryption.html", + "https://www.trendmicro.com/cloudoneconformity-staging/knowledge-base/aws/S3/encrypted-with-kms-customer-master-keys.html", + "https://docs.aws.amazon.com/AmazonS3/latest/userguide/UsingKMSEncryption.html" + ], "Remediation": { "Code": { - "CLI": "aws put-bucket-encryption --bucket --server-side-encryption-configuration '{\"Rules\":[{\"ApplyServerSideEncryptionByDefault\":{\"SSEAlgorithm\":\"aws:kms\",\"KMSMasterKeyID\":\"arn:aws:kms:::key/\"}}]}'", - "NativeIaC": "https://www.trendmicro.com/cloudoneconformity-staging/knowledge-base/aws/S3/encrypted-with-kms-customer-master-keys.html", - "Other": "https://www.trendmicro.com/cloudoneconformity-staging/knowledge-base/aws/S3/encrypted-with-kms-customer-master-keys.html", - "Terraform": "https://docs.prowler.com/checks/aws/general-policies/ensure-that-s3-buckets-are-encrypted-with-kms-by-default#terraform" + "CLI": "aws s3api put-bucket-encryption --bucket --server-side-encryption-configuration '{\"Rules\":[{\"ApplyServerSideEncryptionByDefault\":{\"SSEAlgorithm\":\"aws:kms\"}}]}'", + "NativeIaC": "```yaml\n# CloudFormation: enable default SSE-KMS on the bucket\nResources:\n :\n Type: AWS::S3::Bucket\n Properties:\n BucketEncryption:\n ServerSideEncryptionConfiguration:\n - ServerSideEncryptionByDefault:\n SSEAlgorithm: aws:kms # Critical: sets default encryption to AWS KMS (SSE-KMS)\n```", + "Other": "1. In the AWS Console, go to S3 and open the target bucket\n2. Select the Properties tab\n3. Under Default encryption, click Edit\n4. Choose Server-side encryption with AWS KMS keys (SSE-KMS)\n5. Leave AWS managed key (aws/s3) selected (or choose your CMK if required)\n6. Click Save changes", + "Terraform": "```hcl\n# Enable default SSE-KMS on an existing S3 bucket\nresource \"aws_s3_bucket_server_side_encryption_configuration\" \"\" {\n bucket = \"\"\n\n rule {\n apply_server_side_encryption_by_default {\n sse_algorithm = \"aws:kms\" # Critical: enforces SSE-KMS by default\n }\n }\n}\n```" }, "Recommendation": { - "Text": "Ensure that S3 buckets have encryption at rest enabled using KMS.", - "Url": "https://docs.aws.amazon.com/AmazonS3/latest/userguide/UsingKMSEncryption.html" + "Text": "Enable default **SSE-KMS** (or **DSSE-KMS** for highly sensitive data). Use a customer-managed key, enforce **least privilege** and separation of duties for key usage, and require KMS encryption via bucket policy (specify `aws:kms` and a designated key). Monitor key activity in **CloudTrail** and consider **S3 Bucket Keys** to control cost.", + "Url": "https://hub.prowler.com/check/s3_bucket_kms_encryption" } }, "Categories": [ diff --git a/prowler/providers/aws/services/s3/s3_bucket_level_public_access_block/s3_bucket_level_public_access_block.metadata.json b/prowler/providers/aws/services/s3/s3_bucket_level_public_access_block/s3_bucket_level_public_access_block.metadata.json index e5f014b637..20796001ab 100644 --- a/prowler/providers/aws/services/s3/s3_bucket_level_public_access_block/s3_bucket_level_public_access_block.metadata.json +++ b/prowler/providers/aws/services/s3/s3_bucket_level_public_access_block/s3_bucket_level_public_access_block.metadata.json @@ -1,36 +1,41 @@ { "Provider": "aws", "CheckID": "s3_bucket_level_public_access_block", - "CheckTitle": "Check S3 Bucket Level Public Access Block.", + "CheckTitle": "S3 bucket has Block Public Access with IgnorePublicAcls and RestrictPublicBuckets enabled at bucket or account level", "CheckType": [ - "Data Protection" + "Software and Configuration Checks/AWS Security Best Practices/Network Reachability", + "Software and Configuration Checks/Industry and Regulatory Standards/AWS Foundational Security Best Practices", + "Software and Configuration Checks/Industry and Regulatory Standards/CIS AWS Foundations Benchmark", + "Effects/Data Exposure" ], "ServiceName": "s3", "SubServiceName": "", - "ResourceIdTemplate": "arn:partition:s3:::bucket_name", - "Severity": "medium", + "ResourceIdTemplate": "", + "Severity": "high", "ResourceType": "AwsS3Bucket", "ResourceGroup": "storage", - "Description": "Check S3 Bucket Level Public Access Block.", - "Risk": "Public access policies may be applied to sensitive data buckets.", - "RelatedUrl": "https://docs.aws.amazon.com/AmazonS3/latest/userguide/access-control-block-public-access.html", + "Description": "**Amazon S3 buckets** are evaluated for **Block Public Access** settings, ensuring `ignore_public_acls` and `restrict_public_buckets` are enabled at the bucket or account scope.\n\n*Account-wide protections, when present, are treated as effective for the bucket.*", + "Risk": "Absent **S3 Block Public Access**, public ACLs or broad policies can grant Internet or cross-account access.\n- Data disclosure (confidentiality)\n- Object overwrite or uploads (integrity)\n- Deletion or outages from misuse (availability)", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://www.trendmicro.com/cloudoneconformity/knowledge-base/aws/S3/bucket-public-access-block.html", + "https://docs.aws.amazon.com/AmazonS3/latest/userguide/access-control-block-public-access.html" + ], "Remediation": { "Code": { - "CLI": "aws s3api put-public-access-block --region --public-access-block-configuration BlockPublicAcls=true,IgnorePublicAcls=true,BlockPublicPolicy=true,RestrictPublicBuckets=true --bucket ", - "NativeIaC": "", - "Other": "https://github.com/cloudmatos/matos/tree/master/remediations/aws/s3/s3/block-public-access", - "Terraform": "https://docs.prowler.com/checks/aws/s3-policies/bc_aws_s3_20#terraform" + "CLI": "aws s3api put-public-access-block --bucket --public-access-block-configuration IgnorePublicAcls=true,RestrictPublicBuckets=true", + "NativeIaC": "```yaml\n# CloudFormation - enable required S3 Block Public Access settings on a bucket\nResources:\n :\n Type: AWS::S3::Bucket\n Properties:\n PublicAccessBlockConfiguration:\n IgnorePublicAcls: true # CRITICAL: Ignore public ACLs on the bucket/objects\n RestrictPublicBuckets: true # CRITICAL: Restrict buckets with public policies to same-account/AWS services\n```", + "Other": "1. In AWS Console, open S3 and select the target bucket\n2. Go to Permissions > Block public access (bucket settings)\n3. Enable only:\n - Ignore public ACLs\n - Restrict public buckets\n4. Click Save changes\n5. (Alternatively, to apply account-wide) S3 > Account settings > Block Public Access: enable the same two options and Save", + "Terraform": "```hcl\n# Enable required S3 Block Public Access settings on a bucket\nresource \"aws_s3_bucket_public_access_block\" \"\" {\n bucket = \"\"\n ignore_public_acls = true # CRITICAL: Ignore public ACLs\n restrict_public_buckets = true # CRITICAL: Restrict public buckets\n}\n```" }, "Recommendation": { - "Text": "You can enable Public Access Block at the bucket level to prevent the exposure of your data stored in S3.", - "Url": "https://docs.aws.amazon.com/AmazonS3/latest/userguide/access-control-block-public-access.html" + "Text": "Enable **Block Public Access** at account and bucket levels with `block_public_acls`, `ignore_public_acls`, `block_public_policy`, and `restrict_public_buckets` set to `true`. Apply **least privilege** and **defense in depth**. *If public access is required*, narrowly scope policies to fixed principals and conditions.", + "Url": "https://hub.prowler.com/check/s3_bucket_level_public_access_block" } }, - "Categories": [], - "Tags": { - "Tag1Key": "value", - "Tag2Key": "value" - }, + "Categories": [ + "internet-exposed" + ], "DependsOn": [], "RelatedTo": [], "Notes": "" diff --git a/prowler/providers/aws/services/s3/s3_bucket_lifecycle_enabled/s3_bucket_lifecycle_enabled.metadata.json b/prowler/providers/aws/services/s3/s3_bucket_lifecycle_enabled/s3_bucket_lifecycle_enabled.metadata.json index f7b13296e3..855aed9511 100644 --- a/prowler/providers/aws/services/s3/s3_bucket_lifecycle_enabled/s3_bucket_lifecycle_enabled.metadata.json +++ b/prowler/providers/aws/services/s3/s3_bucket_lifecycle_enabled/s3_bucket_lifecycle_enabled.metadata.json @@ -1,29 +1,36 @@ { "Provider": "aws", "CheckID": "s3_bucket_lifecycle_enabled", - "CheckTitle": "Check if S3 buckets have a Lifecycle configuration enabled", + "CheckTitle": "S3 bucket has a lifecycle configuration enabled", "CheckType": [ - "AWS Foundational Security Best Practices" + "Software and Configuration Checks/AWS Security Best Practices", + "Software and Configuration Checks/Industry and Regulatory Standards/AWS Foundational Security Best Practices" ], "ServiceName": "s3", "SubServiceName": "", - "ResourceIdTemplate": "arn:partition:s3:::bucket_name", + "ResourceIdTemplate": "", "Severity": "low", "ResourceType": "AwsS3Bucket", "ResourceGroup": "storage", - "Description": "Check if S3 buckets have Lifecycle configuration enabled.", - "Risk": "The risks of not having lifecycle management enabled for S3 buckets include higher storage costs, unmanaged data retention, and potential non-compliance with data policies.", - "RelatedUrl": "https://docs.aws.amazon.com/AmazonS3/latest/userguide/object-lifecycle-mgmt.html", + "Description": "**Amazon S3 buckets** use **Lifecycle configurations** with at least one rule `Status: Enabled` to automate object `Transitions` and `Expiration` based on age, prefix, or tags", + "Risk": "Without lifecycle rules, objects persist indefinitely, driving costs and retaining sensitive data beyond policy. Unchecked log/version growth strains operations and recovery. Long-lived data increases exposure if the account is compromised and can break required deletion timelines, affecting confidentiality and availability.", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://docs.aws.amazon.com/securityhub/latest/userguide/s3-controls.html#s3-13", + "https://docs.aws.amazon.com/AmazonS3/latest/userguide/object-lifecycle-mgmt.html", + "https://docs.aws.amazon.com/AmazonS3/latest/userguide/how-to-set-lifecycle-configuration-intro.html", + "https://www.trendmicro.com/cloudoneconformity/knowledge-base/aws/S3/lifecycle-configuration.html" + ], "Remediation": { "Code": { - "CLI": "", - "NativeIaC": "", - "Other": "https://docs.aws.amazon.com/securityhub/latest/userguide/s3-controls.html#s3-13", - "Terraform": "https://www.trendmicro.com/cloudoneconformity/knowledge-base/aws/S3/lifecycle-configuration.html" + "CLI": "aws s3api put-bucket-lifecycle-configuration --bucket --lifecycle-configuration '{\"Rules\":[{\"Status\":\"Enabled\",\"Filter\":{\"Prefix\":\"\"},\"AbortIncompleteMultipartUpload\":{\"DaysAfterInitiation\":7}}]}'", + "NativeIaC": "```yaml\n# CloudFormation: enable a minimal S3 Lifecycle configuration\nResources:\n :\n Type: AWS::S3::Bucket\n Properties:\n LifecycleConfiguration: # CRITICAL: Adds a Lifecycle configuration to the bucket\n Rules:\n - Status: Enabled # CRITICAL: Rule must be Enabled to pass the check\n Filter:\n Prefix: \"\" # Applies to all objects\n AbortIncompleteMultipartUpload:\n DaysAfterInitiation: 7 # Minimal action to satisfy schema\n```", + "Other": "1. In the AWS Console, go to S3 and open the target bucket\n2. Select the Management tab, then click Create lifecycle rule\n3. Enter a name and choose This rule applies to all objects in the bucket\n4. Under Lifecycle rule actions, select Clean up incomplete multipart uploads and set Days after initiation to 7\n5. Ensure Status is Enabled and click Create rule", + "Terraform": "```hcl\n# Minimal lifecycle configuration to mark the bucket as having an enabled rule\nresource \"aws_s3_bucket_lifecycle_configuration\" \"\" {\n bucket = \"\"\n\n rule {\n status = \"Enabled\" # CRITICAL: Enables lifecycle rule to pass the check\n filter {} # Applies to all objects\n\n abort_incomplete_multipart_upload {\n days_after_initiation = 7 # Minimal action to satisfy schema\n }\n }\n}\n```" }, "Recommendation": { - "Text": "Enable lifecycle policies on your S3 buckets to automatically manage the transition and expiration of data.", - "Url": "https://docs.aws.amazon.com/AmazonS3/latest/userguide/how-to-set-lifecycle-configuration-intro.html" + "Text": "Define **Lifecycle policies** by data classification: set `Expiration` to enforce retention, use `Transitions` to lower-cost classes, and enable `AbortIncompleteMultipartUpload`. For critical logs, keep versioning and, *if required*, **Object Lock**. Limit who can change lifecycle using least privilege and separation of duties.", + "Url": "https://hub.prowler.com/check/s3_bucket_lifecycle_enabled" } }, "Categories": [], diff --git a/prowler/providers/aws/services/s3/s3_bucket_no_mfa_delete/s3_bucket_no_mfa_delete.metadata.json b/prowler/providers/aws/services/s3/s3_bucket_no_mfa_delete/s3_bucket_no_mfa_delete.metadata.json index 54d40697ca..1b08dc66ae 100644 --- a/prowler/providers/aws/services/s3/s3_bucket_no_mfa_delete/s3_bucket_no_mfa_delete.metadata.json +++ b/prowler/providers/aws/services/s3/s3_bucket_no_mfa_delete/s3_bucket_no_mfa_delete.metadata.json @@ -1,32 +1,41 @@ { "Provider": "aws", "CheckID": "s3_bucket_no_mfa_delete", - "CheckTitle": "Check if S3 bucket MFA Delete is not enabled.", + "CheckTitle": "S3 bucket has MFA Delete enabled", "CheckType": [ - "Logging and Monitoring" + "Software and Configuration Checks/AWS Security Best Practices", + "Software and Configuration Checks/Industry and Regulatory Standards/AWS Foundational Security Best Practices", + "Software and Configuration Checks/Industry and Regulatory Standards/CIS AWS Foundations Benchmark", + "Effects/Data Destruction" ], "ServiceName": "s3", "SubServiceName": "", - "ResourceIdTemplate": "arn:partition:s3:::bucket_name", + "ResourceIdTemplate": "", "Severity": "medium", "ResourceType": "AwsS3Bucket", "ResourceGroup": "storage", - "Description": "Check if S3 bucket MFA Delete is not enabled.", - "Risk": "Your security credentials are compromised or unauthorized access is granted.", + "Description": "**Amazon S3 buckets** are assessed for **MFA Delete** status. MFA Delete requires a second factor to permanently delete object versions or change `Versioning` configuration. The finding highlights buckets where this protection is not enabled.", + "Risk": "Without **MFA Delete**, a compromised or over-privileged identity can irrevocably purge object history or change versioning.\n\nThis erases recovery points, degrading data **availability**, weakening **integrity**, and increasing the blast radius of account compromise or ransomware.", "RelatedUrl": "", + "AdditionalURLs": [ + "https://docs.aws.amazon.com/AmazonS3/latest/userguide/MultiFactorAuthenticationDelete.html", + "https://docs.aws.amazon.com/AmazonS3/latest/userguide/UsingMFADelete.html" + ], "Remediation": { "Code": { - "CLI": "aws s3api put-bucket-versioning --profile my-root-profile --bucket my-bucket-name --versioning-configuration Status=Enabled,MFADelete=Enabled --mfa 'arn:aws:iam::00000000:mfa/root-account-mfa-device 123456'", + "CLI": "aws s3api put-bucket-versioning --bucket --versioning-configuration Status=Enabled,MFADelete=Enabled --mfa \" \"", "NativeIaC": "", - "Other": "", - "Terraform": "https://docs.prowler.com/checks/aws/s3-policies/bc_aws_s3_24#terraform" + "Other": "1. Sign in to the AWS Management Console as the root user\n2. Open the account menu > Security credentials > Multi-factor authentication (MFA) and assign an MFA device; copy its ARN/serial\n3. From a machine configured to use the root user credentials, run:\n\n```bash\naws s3api put-bucket-versioning --bucket --versioning-configuration Status=Enabled,MFADelete=Enabled --mfa \" \"\n```", + "Terraform": "" }, "Recommendation": { - "Text": "Adding MFA delete to an S3 bucket, requires additional authentication when you change the version state of your bucket or you delete and object version adding another layer of security in the event your security credentials are compromised or unauthorized access is granted.", - "Url": "https://docs.aws.amazon.com/AmazonS3/latest/userguide/MultiFactorAuthenticationDelete.html" + "Text": "Enable **MFA Delete** on sensitive, versioned buckets so permanent deletions and `Versioning` changes require a second factor. Apply **least privilege** to restrict version purge actions, enforce **change control**, and combine with **Object Lock** or immutable backups for defense in depth.", + "Url": "https://hub.prowler.com/check/s3_bucket_no_mfa_delete" } }, - "Categories": [], + "Categories": [ + "identity-access" + ], "DependsOn": [], "RelatedTo": [], "Notes": "" diff --git a/prowler/providers/aws/services/s3/s3_bucket_object_lock/s3_bucket_object_lock.metadata.json b/prowler/providers/aws/services/s3/s3_bucket_object_lock/s3_bucket_object_lock.metadata.json index 54a639a61c..018902cb4c 100644 --- a/prowler/providers/aws/services/s3/s3_bucket_object_lock/s3_bucket_object_lock.metadata.json +++ b/prowler/providers/aws/services/s3/s3_bucket_object_lock/s3_bucket_object_lock.metadata.json @@ -1,32 +1,41 @@ { "Provider": "aws", "CheckID": "s3_bucket_object_lock", - "CheckTitle": "Check if S3 buckets have object lock enabled", + "CheckTitle": "S3 bucket has Object Lock enabled", "CheckType": [ - "Data Protection" + "Software and Configuration Checks/AWS Security Best Practices", + "Effects/Data Destruction" ], "ServiceName": "s3", "SubServiceName": "", - "ResourceIdTemplate": "arn:partition:s3:::bucket_name", + "ResourceIdTemplate": "", "Severity": "low", "ResourceType": "AwsS3Bucket", "ResourceGroup": "storage", - "Description": "Check if S3 buckets have object lock enabled", - "Risk": "Store objects using a write-once-read-many (WORM) model to help you prevent objects from being deleted or overwritten for a fixed amount of time or indefinitely. That helps to prevent ransomware attacks.", + "Description": "**Amazon S3 buckets** have **Object Lock** enabled at the bucket level, applying WORM controls to object versions", + "Risk": "Without **Object Lock**, object versions can be deleted or overwritten, undermining data **integrity** and **availability**.\n\nThreats include ransomware erasing backups, insider or mistaken deletions, and tampering that defeats recovery. Inability to enforce retention or legal holds increases exposure to data loss and compliance gaps.", "RelatedUrl": "", + "AdditionalURLs": [ + "https://docs.aws.amazon.com/AmazonS3/latest/userguide/object-lock-overview.html", + "https://aws.amazon.com/about-aws/whats-new/2018/11/s3-object-lock/", + "https://www.trendmicro.com/cloudoneconformity/knowledge-base/aws/S3/object-lock.html", + "https://docs.aws.amazon.com/de_de/cli/latest/reference/s3api/put-object-lock-configuration.html" + ], "Remediation": { "Code": { - "CLI": "aws s3 put-object-lock-configuration --bucket --object-lock-configuration '{\"ObjectLockEnabled\":\"Enabled\",\"Rule\":{\"DefaultRetention\":{\"Mode\":\"GOVERNANCE\",\"Days\":1}}}'", - "NativeIaC": "", - "Other": "https://www.trendmicro.com/cloudoneconformity/knowledge-base/aws/S3/object-lock.html", - "Terraform": "https://docs.prowler.com/checks/aws/general-policies/ensure-that-s3-bucket-has-lock-configuration-enabled-by-default#terraform" + "CLI": "aws s3api put-object-lock-configuration --bucket --object-lock-configuration '{\"ObjectLockEnabled\":\"Enabled\",\"Rule\":{\"DefaultRetention\":{\"Mode\":\"GOVERNANCE\",\"Days\":1}}}'", + "NativeIaC": "```yaml\nResources:\n :\n Type: AWS::S3::Bucket\n Properties:\n ObjectLockEnabled: true # CRITICAL: Enables Object Lock on the bucket at creation\n ObjectLockConfiguration:\n ObjectLockEnabled: Enabled # CRITICAL: Turns on Object Lock configuration\n Rule:\n DefaultRetention:\n Mode: GOVERNANCE # CRITICAL: Sets default retention mode\n Days: 1 # CRITICAL: Minimal retention to enable default lock\n```", + "Other": "1. Open the AWS S3 console and select the target bucket\n2. Go to the Properties tab\n3. Find Object Lock and click Edit\n4. Enable Object Lock\n5. Set Default retention: Mode = Governance, Days = 1\n6. Click Save changes\n\nNote: If the bucket was not created with Object Lock, create a new bucket with Object Lock enabled and migrate objects.", + "Terraform": "```hcl\nresource \"aws_s3_bucket_object_lock_configuration\" \"\" {\n bucket = \"\"\n object_lock_enabled = \"Enabled\" # CRITICAL: Enables Object Lock configuration on the bucket\n rule {\n default_retention {\n mode = \"GOVERNANCE\" # CRITICAL: Default retention mode\n days = 1 # CRITICAL: Minimal retention period\n }\n }\n}\n```" }, "Recommendation": { - "Text": "Ensure that your Amazon S3 buckets have Object Lock feature enabled in order to prevent the objects they store from being deleted.", - "Url": "https://docs.aws.amazon.com/AmazonS3/latest/userguide/object-lock-overview.html" + "Text": "Enable **Object Lock** for critical data and set appropriate default retention in `GOVERNANCE` or `COMPLIANCE` mode.\n\nApply **immutability** and **least privilege** by restricting permissions that bypass retention, and use legal holds when you need indefinite protection for investigations or regulatory requirements.", + "Url": "https://hub.prowler.com/check/s3_bucket_object_lock" } }, - "Categories": [], + "Categories": [ + "resilience" + ], "DependsOn": [], "RelatedTo": [], "Notes": "" diff --git a/prowler/providers/aws/services/s3/s3_bucket_object_versioning/s3_bucket_object_versioning.metadata.json b/prowler/providers/aws/services/s3/s3_bucket_object_versioning/s3_bucket_object_versioning.metadata.json index 5b8d9e5d56..0b5b42e789 100644 --- a/prowler/providers/aws/services/s3/s3_bucket_object_versioning/s3_bucket_object_versioning.metadata.json +++ b/prowler/providers/aws/services/s3/s3_bucket_object_versioning/s3_bucket_object_versioning.metadata.json @@ -1,32 +1,40 @@ { "Provider": "aws", "CheckID": "s3_bucket_object_versioning", - "CheckTitle": "Check if S3 buckets have object versioning enabled", + "CheckTitle": "S3 bucket has object versioning enabled", "CheckType": [ - "Data Protection" + "Software and Configuration Checks/AWS Security Best Practices", + "Industry and Regulatory Standards/AWS Foundational Security Best Practices", + "Industry and Regulatory Standards/CIS AWS Foundations Benchmark", + "Effects/Data Destruction" ], "ServiceName": "s3", "SubServiceName": "", - "ResourceIdTemplate": "arn:partition:s3:::bucket_name", + "ResourceIdTemplate": "", "Severity": "medium", "ResourceType": "AwsS3Bucket", "ResourceGroup": "storage", - "Description": "Check if S3 buckets have object versioning enabled", - "Risk": "With versioning, you can easily recover from both unintended user actions and application failures.", + "Description": "**Amazon S3 buckets** are evaluated for **object versioning** being `Enabled`, which maintains multiple versions of the same object key for historical state retention", + "Risk": "Without **versioning**, deletions and overwrites remove the only copy, undermining **availability** and **integrity**.\n- Compromised identities or buggy apps can mass-delete/corrupt data\n- No historical versions means limited rollback and irrecoverable loss", "RelatedUrl": "", + "AdditionalURLs": [ + "https://docs.aws.amazon.com/AmazonS3/latest/dev-retired/Versioning.html" + ], "Remediation": { "Code": { - "CLI": "", - "NativeIaC": "", - "Other": "https://docs.prowler.com/checks/aws/s3-policies/s3_16-enable-versioning#aws-console", - "Terraform": "https://docs.prowler.com/checks/aws/s3-policies/s3_16-enable-versioning#terraform" + "CLI": "aws s3api put-bucket-versioning --bucket --versioning-configuration Status=Enabled", + "NativeIaC": "```yaml\n# CloudFormation: enable versioning on an S3 bucket\nResources:\n :\n Type: AWS::S3::Bucket\n Properties:\n VersioningConfiguration:\n Status: Enabled # Critical: turns on object versioning to pass the check\n```", + "Other": "1. In the AWS Console, go to S3 > Buckets and select the target bucket\n2. Open the Properties tab\n3. Find Bucket Versioning and click Edit\n4. Select Enable and click Save changes", + "Terraform": "```hcl\n# Enable versioning on an existing S3 bucket\nresource \"aws_s3_bucket_versioning\" \"\" {\n bucket = \"\"\n versioning_configuration {\n status = \"Enabled\" # Critical: enables bucket versioning to remediate the finding\n }\n}\n```" }, "Recommendation": { - "Text": "Configure versioning using the Amazon console or API for buckets with sensitive information that is changing frequently, and backup may not be enough to capture all the changes.", - "Url": "https://docs.aws.amazon.com/AmazonS3/latest/userguide/Versioning.html" + "Text": "Enable **S3 versioning** for buckets holding important or shared data.\n- Enforce **least privilege** to limit delete/overwrite\n- Use **Object Lock** and/or **MFA Delete** for stronger protection\n- Apply **lifecycle rules** to manage noncurrent versions and costs\n- Layer with backups/replication for **defense in depth**", + "Url": "https://hub.prowler.com/check/s3_bucket_object_versioning" } }, - "Categories": [], + "Categories": [ + "resilience" + ], "DependsOn": [], "RelatedTo": [], "Notes": "" diff --git a/prowler/providers/aws/services/s3/s3_bucket_policy_public_write_access/s3_bucket_policy_public_write_access.metadata.json b/prowler/providers/aws/services/s3/s3_bucket_policy_public_write_access/s3_bucket_policy_public_write_access.metadata.json index 19a823ba64..784944d2d9 100644 --- a/prowler/providers/aws/services/s3/s3_bucket_policy_public_write_access/s3_bucket_policy_public_write_access.metadata.json +++ b/prowler/providers/aws/services/s3/s3_bucket_policy_public_write_access/s3_bucket_policy_public_write_access.metadata.json @@ -1,29 +1,36 @@ { "Provider": "aws", "CheckID": "s3_bucket_policy_public_write_access", - "CheckTitle": "Check if S3 buckets have policies which allow WRITE access.", + "CheckTitle": "S3 bucket policy does not allow public write access", "CheckType": [ - "IAM" + "Software and Configuration Checks/AWS Security Best Practices", + "Software and Configuration Checks/Industry and Regulatory Standards/AWS Foundational Security Best Practices", + "Software and Configuration Checks/Industry and Regulatory Standards/CIS AWS Foundations Benchmark", + "Effects/Data Destruction", + "TTPs/Initial Access" ], "ServiceName": "s3", "SubServiceName": "", - "ResourceIdTemplate": "arn:partition:s3:::bucket_name", + "ResourceIdTemplate": "", "Severity": "critical", "ResourceType": "AwsS3Bucket", "ResourceGroup": "storage", - "Description": "Check if S3 buckets have policies which allow WRITE access.", - "Risk": "Non intended users can put objects in a given bucket.", + "Description": "**Amazon S3 bucket policies** are evaluated for **public write permissions** (e.g., `s3:PutObject`, `s3:Delete*`, or `s3:*`). Account or bucket **Public Access Block** that restricts public buckets is considered when determining exposure.", + "Risk": "Public write access lets anyone upload, overwrite, or delete objects, undermining **integrity** and **availability**. Attackers can plant malware, stage phishing content, poison data, or wipe buckets, causing outages and potential legal and cost impacts from storage abuse and content hosting.", "RelatedUrl": "", + "AdditionalURLs": [ + "https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_examples_s3_rw-bucket.html" + ], "Remediation": { "Code": { - "CLI": "", - "NativeIaC": "", - "Other": "https://docs.prowler.com/checks/aws/s3-policies/s3_18-write-permissions-public#aws-console", - "Terraform": "" + "CLI": "aws s3api put-public-access-block --bucket --public-access-block-configuration RestrictPublicBuckets=true", + "NativeIaC": "```yaml\n# CloudFormation: Enable bucket-level Public Access Block to prevent public policies from granting write\nResources:\n :\n Type: AWS::S3::Bucket\n Properties:\n PublicAccessBlockConfiguration:\n RestrictPublicBuckets: true # Critical: blocks public access granted by any public bucket policy\n```", + "Other": "1. Open the AWS S3 console and select the target bucket\n2. Go to the Permissions tab\n3. Under Block public access (bucket settings), click Edit\n4. Enable \"Block public and cross-account access to buckets and objects through any public bucket or access point policies\"\n5. Click Save changes", + "Terraform": "```hcl\n# Enable bucket-level Public Access Block so public bucket policies (including write) are blocked\nresource \"aws_s3_bucket_public_access_block\" \"\" {\n bucket = \"\"\n restrict_public_buckets = true # Critical: prevents public access granted via bucket policies\n}\n```" }, "Recommendation": { - "Text": "Ensure proper bucket policy is in place with the least privilege principle applied.", - "Url": "https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_examples_s3_rw-bucket.html" + "Text": "Restrict writes to trusted principals using **least privilege**; avoid `Principal: \"*\"`. Enable **Public Access Block** at account and bucket levels for defense in depth. Prefer IAM roles over broad bucket policies, require private access paths, and enable versioning to recover from unwanted changes.", + "Url": "https://hub.prowler.com/check/s3_bucket_policy_public_write_access" } }, "Categories": [ diff --git a/prowler/providers/aws/services/s3/s3_bucket_public_access/s3_bucket_public_access.metadata.json b/prowler/providers/aws/services/s3/s3_bucket_public_access/s3_bucket_public_access.metadata.json index 17bc58f884..97d4332e44 100644 --- a/prowler/providers/aws/services/s3/s3_bucket_public_access/s3_bucket_public_access.metadata.json +++ b/prowler/providers/aws/services/s3/s3_bucket_public_access/s3_bucket_public_access.metadata.json @@ -1,29 +1,36 @@ { "Provider": "aws", "CheckID": "s3_bucket_public_access", - "CheckTitle": "Ensure there are no S3 buckets open to Everyone or Any AWS user.", + "CheckTitle": "S3 bucket is not publicly accessible to Everyone or Authenticated Users", "CheckType": [ - "Data Protection" + "Software and Configuration Checks/AWS Security Best Practices/Network Reachability", + "Software and Configuration Checks/Industry and Regulatory Standards/AWS Foundational Security Best Practices", + "Software and Configuration Checks/Industry and Regulatory Standards/CIS AWS Foundations Benchmark", + "Effects/Data Exposure" ], "ServiceName": "s3", "SubServiceName": "", - "ResourceIdTemplate": "arn:partition:s3:::bucket_name", + "ResourceIdTemplate": "", "Severity": "critical", "ResourceType": "AwsS3Bucket", "ResourceGroup": "storage", - "Description": "Ensure there are no S3 buckets open to Everyone or Any AWS user.", - "Risk": "Even if you enable all possible bucket ACL options available in the Amazon S3 console the ACL alone does not allow everyone to download objects from your bucket. Depending on which option you select any user could perform some actions.", + "Description": "**Amazon S3 buckets** are evaluated for **public access** via ACLs and bucket policies. The check identifies account or bucket `PublicAccessBlock` protections (`IgnorePublicAcls`, `RestrictPublicBuckets`) and flags buckets granting group access to `AllUsers` or `AuthenticatedUsers`, or with a public bucket policy.", + "Risk": "Publicly accessible buckets jeopardize **confidentiality** through unauthenticated reads, **integrity** through write or ACL changes, and **availability** via object deletion or overwrite. Attackers can mass-exfiltrate data, host malware, or pivot after discovering secrets stored in objects.", "RelatedUrl": "", + "AdditionalURLs": [ + "https://docs.aws.amazon.com/id_id/kitchensink/latest/testguide/access-control-block-public-access.html", + "https://docs.aws.amazon.com/AmazonS3/latest/userguide/access-control-block-public-access.html" + ], "Remediation": { "Code": { - "CLI": "aws s3api put-public-access-block --public-access-block-configuration BlockPublicAcls=true,IgnorePublicAcls=true,BlockPublicPolicy=true,RestrictPublicBuckets=true --bucket ", - "NativeIaC": "", - "Other": "https://github.com/cloudmatos/matos/tree/master/remediations/aws/s3/s3/block-public-access", - "Terraform": "https://docs.prowler.com/checks/aws/networking-policies/s3-bucket-should-have-public-access-blocks-defaults-to-false-if-the-public-access-block-is-not-attached#terraform" + "CLI": "aws s3api put-public-access-block --bucket --public-access-block-configuration IgnorePublicAcls=true,RestrictPublicBuckets=true", + "NativeIaC": "```yaml\n# CloudFormation: Enable minimal S3 Block Public Access on the bucket\nResources:\n ExampleBucket:\n Type: AWS::S3::Bucket\n Properties:\n PublicAccessBlockConfiguration:\n IgnorePublicAcls: true # Critical: ignores any public ACL grants (AllUsers/AuthenticatedUsers)\n RestrictPublicBuckets: true # Critical: restricts buckets with public policies to same-account/service principals\n```", + "Other": "1. In the AWS Console, go to S3 and open the bucket \n2. Select the Permissions tab\n3. Under Block public access (bucket settings), click Edit\n4. Check only:\n - Ignore public ACLs (true)\n - Restrict public buckets (true)\n5. Click Save changes and confirm", + "Terraform": "```hcl\n# Enable minimal S3 Block Public Access on the bucket\nresource \"aws_s3_bucket_public_access_block\" \"example\" {\n bucket = \"\"\n\n ignore_public_acls = true # Critical: ignores public ACLs\n restrict_public_buckets = true # Critical: restricts buckets with public policies\n}\n```" }, "Recommendation": { - "Text": "You can enable block public access settings only for access points, buckets and AWS accounts. Amazon S3 does not support block public access settings on a per-object basis. When you apply block public access settings to an account, the settings apply to all AWS Regions globally. The settings might not take effect in all Regions immediately or simultaneously, but they eventually propagate to all Regions.", - "Url": "https://docs.aws.amazon.com/AmazonS3/latest/userguide/access-control-block-public-access.html" + "Text": "Enforce defense in depth: enable **S3 Block Public Access** at org/account and bucket levels (`BlockPublicAcls`, `IgnorePublicAcls`, `BlockPublicPolicy`, `RestrictPublicBuckets`). Apply **least privilege** with explicit principals; avoid ACLs via Object Ownership. Use private access patterns (e.g., CloudFront OAC or presigned URLs) and monitor with analyzers.", + "Url": "https://hub.prowler.com/check/s3_bucket_public_access" } }, "Categories": [ diff --git a/prowler/providers/aws/services/s3/s3_bucket_public_list_acl/s3_bucket_public_list_acl.metadata.json b/prowler/providers/aws/services/s3/s3_bucket_public_list_acl/s3_bucket_public_list_acl.metadata.json index 6376694d3b..53d1bd6351 100644 --- a/prowler/providers/aws/services/s3/s3_bucket_public_list_acl/s3_bucket_public_list_acl.metadata.json +++ b/prowler/providers/aws/services/s3/s3_bucket_public_list_acl/s3_bucket_public_list_acl.metadata.json @@ -1,29 +1,36 @@ { "Provider": "aws", "CheckID": "s3_bucket_public_list_acl", - "CheckTitle": "Ensure there are no S3 buckets listable by Everyone or Any AWS customer.", + "CheckTitle": "S3 bucket is not publicly listable by Everyone or any authenticated AWS user", "CheckType": [ - "Data Protection" + "Software and Configuration Checks/AWS Security Best Practices/Network Reachability", + "Software and Configuration Checks/Industry and Regulatory Standards/AWS Foundational Security Best Practices", + "Effects/Data Exposure", + "TTPs/Initial Access/Unauthorized Access" ], "ServiceName": "s3", "SubServiceName": "", - "ResourceIdTemplate": "arn:partition:s3:::bucket_name", + "ResourceIdTemplate": "", "Severity": "critical", "ResourceType": "AwsS3Bucket", "ResourceGroup": "storage", - "Description": "Ensure there are no S3 buckets listable by Everyone or Any AWS customer.", - "Risk": "Even if you enable all possible bucket ACL options available in the Amazon S3 console the ACL alone does not allow everyone to download objects from your bucket. Depending on which option you select any user could perform some actions.", + "Description": "**Amazon S3 buckets** are evaluated for **public listing via ACLs**. Grants of `READ`, `READ_ACP`, or `FULL_CONTROL` to the `AllUsers` or `AuthenticatedUsers` groups are identified. Effective **Block Public Access** at account or bucket level (notably `IgnorePublicAcls` and `RestrictPublicBuckets`) is considered in the evaluation.", + "Risk": "**Public listability** reveals object names, counts, and structure, enabling reconnaissance and targeted scraping. `READ_ACP` exposes permission details for further abuse. With `FULL_CONTROL`, attackers could alter ACLs and disrupt access, undermining **confidentiality** and risking **integrity** and **availability**.", "RelatedUrl": "", + "AdditionalURLs": [ + "https://docs.aws.amazon.com/AmazonS3/latest/userguide/access-control-block-public-access.html", + "https://www.trendmicro.com/cloudoneconformity-staging/knowledge-base/aws/S3/s3-bucket-public-read-access.html" + ], "Remediation": { "Code": { - "CLI": "aws s3api put-bucket-acl --bucket --acl private", - "NativeIaC": "https://www.trendmicro.com/cloudoneconformity-staging/knowledge-base/aws/S3/s3-bucket-public-read-access.html", - "Other": "", - "Terraform": "https://www.trendmicro.com/cloudoneconformity-staging/knowledge-base/aws/S3/s3-bucket-public-read-access.html" + "CLI": "aws s3api put-public-access-block --bucket --public-access-block-configuration IgnorePublicAcls=true,RestrictPublicBuckets=true", + "NativeIaC": "```yaml\n# CloudFormation: Block public listing via bucket-level Public Access Block\nResources:\n ExamplePublicAccessBlock:\n Type: AWS::S3::BucketPublicAccessBlock\n Properties:\n Bucket: \"\"\n PublicAccessBlockConfiguration:\n IgnorePublicAcls: true # Critical: ignore any public ACLs so they don't grant list access\n RestrictPublicBuckets: true # Critical: restrict buckets with public policies to trusted principals\n```", + "Other": "1. In the AWS Console, go to S3 and open the bucket\n2. Select the Permissions tab\n3. Click Edit under Block public access (bucket settings)\n4. Enable:\n - Ignore public ACLs (bucket and objects)\n - Restrict public buckets\n5. Click Save", + "Terraform": "```hcl\n# Block public listing by enabling bucket-level Public Access Block\nresource \"aws_s3_bucket_public_access_block\" \"\" {\n bucket = \"\"\n ignore_public_acls = true # Critical: ignore any public ACLs\n restrict_public_buckets = true # Critical: restrict buckets with public policies\n}\n```" }, "Recommendation": { - "Text": "You can enable block public access settings only for access points, buckets and AWS accounts. Amazon S3 does not support block public access settings on a per-object basis. When you apply block public access settings to an account, the settings apply to all AWS Regions globally. The settings might not take effect in all Regions immediately or simultaneously, but they eventually propagate to all Regions.", - "Url": "https://docs.aws.amazon.com/AmazonS3/latest/userguide/access-control-block-public-access.html" + "Text": "Enable account-level **S3 Block Public Access** (`BlockPublicAcls`, `IgnorePublicAcls`, `BlockPublicPolicy`, `RestrictPublicBuckets`).\n- Remove ACL grants to `AllUsers`/`AuthenticatedUsers`; apply **least privilege** with IAM/bucket policies.\n- Favor private patterns (VPC endpoints, CloudFront OAC, presigned URLs) and disable ACLs via Object Ownership.", + "Url": "https://hub.prowler.com/check/s3_bucket_public_list_acl" } }, "Categories": [ diff --git a/prowler/providers/aws/services/s3/s3_bucket_public_write_acl/s3_bucket_public_write_acl.metadata.json b/prowler/providers/aws/services/s3/s3_bucket_public_write_acl/s3_bucket_public_write_acl.metadata.json index d540e2bc66..7a00a61494 100644 --- a/prowler/providers/aws/services/s3/s3_bucket_public_write_acl/s3_bucket_public_write_acl.metadata.json +++ b/prowler/providers/aws/services/s3/s3_bucket_public_write_acl/s3_bucket_public_write_acl.metadata.json @@ -1,29 +1,37 @@ { "Provider": "aws", "CheckID": "s3_bucket_public_write_acl", - "CheckTitle": "Ensure there are no S3 buckets writable by Everyone or Any AWS customer.", + "CheckTitle": "S3 bucket ACL does not grant write access to Everyone or any AWS customer", "CheckType": [ - "Data Protection" + "Software and Configuration Checks/AWS Security Best Practices", + "Software and Configuration Checks/Industry and Regulatory Standards/AWS Foundational Security Best Practices", + "Software and Configuration Checks/Industry and Regulatory Standards/CIS AWS Foundations Benchmark", + "TTPs/Initial Access/Unauthorized Access", + "Effects/Data Destruction" ], "ServiceName": "s3", "SubServiceName": "", - "ResourceIdTemplate": "arn:partition:s3:::bucket_name", + "ResourceIdTemplate": "", "Severity": "critical", "ResourceType": "AwsS3Bucket", "ResourceGroup": "storage", - "Description": "Ensure there are no S3 buckets writable by Everyone or Any AWS customer.", - "Risk": "Even if you enable all possible bucket ACL options available in the Amazon S3 console the ACL alone does not allow everyone to download objects from your bucket. Depending on which option you select any user could perform some actions.", + "Description": "**Amazon S3 buckets** are assessed for ACL grants that allow **public write** access to `AllUsers` or `AuthenticatedUsers` via `WRITE`, `WRITE_ACP`, or `FULL_CONTROL`. Effective **Block Public Access** at account or bucket level (`ignore_public_acls`, `restrict_public_buckets`) is considered.", + "Risk": "Public or cross-account writes enable object tampering, **log poisoning**, and ACL changes via `WRITE_ACP`, undermining **integrity** and causing covert **data exposure**. Attackers can plant malware, deface content, and inflate **costs**, impacting **availability** through overwrites or prefix flooding.", "RelatedUrl": "", + "AdditionalURLs": [ + "https://docs.aws.amazon.com/AmazonS3/latest/userguide/access-control-block-public-access.html", + "https://www.trendmicro.com/cloudoneconformity-staging/knowledge-base/aws/S3/s3-bucket-public-write-access.html" + ], "Remediation": { "Code": { "CLI": "aws s3api put-bucket-acl --bucket --acl private", - "NativeIaC": "https://www.trendmicro.com/cloudoneconformity-staging/knowledge-base/aws/S3/s3-bucket-public-write-access.html", - "Other": "", - "Terraform": "https://www.trendmicro.com/cloudoneconformity-staging/knowledge-base/aws/S3/s3-bucket-public-write-access.html" + "NativeIaC": "```yaml\n# CloudFormation: ensure bucket is not publicly writable via ACLs\nResources:\n :\n Type: AWS::S3::Bucket\n Properties:\n PublicAccessBlockConfiguration:\n IgnorePublicAcls: true # Critical: ignores any public ACLs (e.g., AllUsers/AuthenticatedUsers) so write grants don't apply\n RestrictPublicBuckets: true # Critical: restricts public buckets to the account, preventing public writes via policies\n```", + "Other": "1. In the AWS Console, go to S3 > Buckets and open \n2. Go to the Permissions tab > Access control list (ACL) > Edit\n3. Remove any grantee \"Everyone (public access)\" or \"Any AWS account\" with Write, Write ACL, or Full control\n4. Ensure only the bucket owner retains Full control\n5. Click Save changes", + "Terraform": "```hcl\n# Ensure the bucket is not publicly writable via ACLs\nresource \"aws_s3_bucket_public_access_block\" \"\" {\n bucket = \"\"\n ignore_public_acls = true # Critical: disables effect of public ACLs (e.g., AllUsers/AuthenticatedUsers)\n restrict_public_buckets = true # Critical: restricts public buckets to the account to prevent public writes\n}\n```" }, "Recommendation": { - "Text": "You can enable block public access settings only for access points, buckets and AWS accounts. Amazon S3 does not support block public access settings on a per-object basis. When you apply block public access settings to an account, the settings apply to all AWS Regions globally. The settings might not take effect in all Regions immediately or simultaneously, but they eventually propagate to all Regions.", - "Url": "https://docs.aws.amazon.com/AmazonS3/latest/userguide/access-control-block-public-access.html" + "Text": "Apply **least privilege** to S3 writes. Enable account-level **Block Public Access** and use **Object Ownership** to disable ACLs. Grant write only to fixed principals via bucket policies with tight conditions (e.g., org IDs, VPC endpoints). Add **versioning** and monitoring for defense-in-depth.", + "Url": "https://hub.prowler.com/check/s3_bucket_public_write_acl" } }, "Categories": [ diff --git a/prowler/providers/aws/services/s3/s3_bucket_secure_transport_policy/s3_bucket_secure_transport_policy.metadata.json b/prowler/providers/aws/services/s3/s3_bucket_secure_transport_policy/s3_bucket_secure_transport_policy.metadata.json index 709c136297..020e6685e0 100644 --- a/prowler/providers/aws/services/s3/s3_bucket_secure_transport_policy/s3_bucket_secure_transport_policy.metadata.json +++ b/prowler/providers/aws/services/s3/s3_bucket_secure_transport_policy/s3_bucket_secure_transport_policy.metadata.json @@ -1,29 +1,35 @@ { "Provider": "aws", "CheckID": "s3_bucket_secure_transport_policy", - "CheckTitle": "Check if S3 buckets have secure transport policy.", + "CheckTitle": "S3 bucket policy denies requests over insecure transport", "CheckType": [ - "Data Protection" + "Software and Configuration Checks/AWS Security Best Practices/Network Reachability", + "Software and Configuration Checks/Industry and Regulatory Standards/AWS Foundational Security Best Practices", + "Software and Configuration Checks/Industry and Regulatory Standards/CIS AWS Foundations Benchmark" ], "ServiceName": "s3", "SubServiceName": "", - "ResourceIdTemplate": "arn:partition:s3:::bucket_name", + "ResourceIdTemplate": "", "Severity": "medium", "ResourceType": "AwsS3Bucket", "ResourceGroup": "storage", - "Description": "Check if S3 buckets have secure transport policy.", - "Risk": "If HTTPS is not enforced on the bucket policy, communication between clients and S3 buckets can use unencrypted HTTP. As a result, sensitive information could be transmitted in clear text over the network or internet.", + "Description": "**Amazon S3 buckets** are evaluated for a bucket policy that enforces **secure transport** by denying requests when `aws:SecureTransport` is `false`.\n\nBuckets without this explicit denial, or without a policy, are treated as allowing access over insecure transport.", + "Risk": "HTTP access exposes object data and auth details to **eavesdropping** and **man-in-the-middle** attacks. Captured **pre-signed URLs** can be replayed to exfiltrate data. Traffic can be intercepted or altered, undermining **confidentiality** and **integrity** of S3 content.", "RelatedUrl": "", + "AdditionalURLs": [ + "https://www.trendmicro.com/cloudoneconformity/knowledge-base/aws/S3/secure-transport.html", + "https://aws.amazon.com/premiumsupport/knowledge-center/s3-bucket-policy-for-config-rule/" + ], "Remediation": { "Code": { - "CLI": "", - "NativeIaC": "", - "Other": "https://docs.prowler.com/checks/aws/s3-policies/s3_15-secure-data-transport#aws-console", - "Terraform": "" + "CLI": "aws s3api put-bucket-policy --bucket --policy '{\"Version\":\"2012-10-17\",\"Statement\":[{\"Effect\":\"Deny\",\"Principal\":\"*\",\"Action\":\"s3:*\",\"Resource\":\"arn:aws:s3:::/*\",\"Condition\":{\"Bool\":{\"aws:SecureTransport\":\"false\"}}}]}'", + "NativeIaC": "```yaml\n# CloudFormation: Deny non-SSL (HTTP) requests to the S3 bucket\nResources:\n BucketPolicy:\n Type: AWS::S3::BucketPolicy\n Properties:\n Bucket: \n PolicyDocument:\n Version: '2012-10-17'\n Statement:\n - Effect: Deny\n Principal: \"*\"\n Action: s3:*\n Resource: arn:aws:s3:::/*\n Condition:\n Bool:\n aws:SecureTransport: \"false\" # Critical: deny requests not using SSL/TLS\n```", + "Other": "1. In the AWS Console, go to S3 and open the bucket \n2. Select the Permissions tab and click Edit in Bucket policy\n3. Paste this policy, replacing the bucket name:\n ```\n {\n \"Version\": \"2012-10-17\",\n \"Statement\": [\n {\n \"Effect\": \"Deny\",\n \"Principal\": \"*\",\n \"Action\": \"s3:*\",\n \"Resource\": \"arn:aws:s3:::/*\",\n \"Condition\": { \"Bool\": { \"aws:SecureTransport\": \"false\" } }\n }\n ]\n }\n ```\n4. Click Save changes\n", + "Terraform": "```hcl\n# Deny non-SSL (HTTP) requests to the S3 bucket\nresource \"aws_s3_bucket_policy\" \"policy\" {\n bucket = \"\"\n policy = jsonencode({\n Version = \"2012-10-17\"\n Statement = [{\n Effect = \"Deny\"\n Principal = \"*\"\n Action = \"s3:*\"\n Resource = \"arn:aws:s3:::/*\"\n Condition = {\n Bool = {\n \"aws:SecureTransport\" = \"false\" # Critical: deny requests not using SSL/TLS\n }\n }\n }]\n })\n}\n```" }, "Recommendation": { - "Text": "Ensure that S3 buckets have encryption in transit enabled.", - "Url": "https://aws.amazon.com/premiumsupport/knowledge-center/s3-bucket-policy-for-config-rule/" + "Text": "Enforce **HTTPS-only** access with a bucket policy that denies requests when `aws:SecureTransport=false`.\n\nPrefer **private access** (VPC endpoints or CloudFront with TLS), avoid S3 website endpoints, apply **least privilege**, use short-lived HTTPS **pre-signed URLs**, and monitor logs for insecure access attempts.", + "Url": "https://hub.prowler.com/check/s3_bucket_secure_transport_policy" } }, "Categories": [ diff --git a/prowler/providers/aws/services/s3/s3_bucket_server_access_logging_enabled/s3_bucket_server_access_logging_enabled.metadata.json b/prowler/providers/aws/services/s3/s3_bucket_server_access_logging_enabled/s3_bucket_server_access_logging_enabled.metadata.json index 67fe62f840..3b0e811b4c 100644 --- a/prowler/providers/aws/services/s3/s3_bucket_server_access_logging_enabled/s3_bucket_server_access_logging_enabled.metadata.json +++ b/prowler/providers/aws/services/s3/s3_bucket_server_access_logging_enabled/s3_bucket_server_access_logging_enabled.metadata.json @@ -1,32 +1,39 @@ { "Provider": "aws", "CheckID": "s3_bucket_server_access_logging_enabled", - "CheckTitle": "Check if S3 buckets have server access logging enabled", + "CheckTitle": "S3 bucket has server access logging enabled", "CheckType": [ - "Logging and Monitoring" + "Software and Configuration Checks/AWS Security Best Practices", + "Software and Configuration Checks/Industry and Regulatory Standards/AWS Foundational Security Best Practices", + "Software and Configuration Checks/Industry and Regulatory Standards/CIS AWS Foundations Benchmark" ], "ServiceName": "s3", "SubServiceName": "", - "ResourceIdTemplate": "arn:partition:s3:::bucket_name", + "ResourceIdTemplate": "", "Severity": "medium", "ResourceType": "AwsS3Bucket", "ResourceGroup": "storage", - "Description": "Check if S3 buckets have server access logging enabled", - "Risk": "Server access logs can assist you in security and access audits, help you learn about your customer base, and understand your Amazon S3 bill.", + "Description": "**Amazon S3 buckets** are evaluated for **server access logging** configured to record access requests and deliver logs to a designated destination bucket.", + "Risk": "Without access logs, object reads, writes, and deletions may go untracked, hindering detection of unauthorized access and data exfiltration. This degrades forensic visibility, delays incident response, and weakens evidence integrity, impacting confidentiality and integrity.", "RelatedUrl": "", + "AdditionalURLs": [ + "https://docs.amazonaws.cn/en_us/config/latest/developerguide/s3-bucket-logging-enabled.html", + "https://docs.aws.amazon.com/AmazonS3/latest/dev/security-best-practices.html" + ], "Remediation": { "Code": { - "CLI": "aws s3api put-bucket-logging --bucket --bucket-logging-status ", - "NativeIaC": "", - "Other": "https://docs.prowler.com/checks/aws/s3-policies/s3_13-enable-logging", - "Terraform": "https://docs.prowler.com/checks/aws/s3-policies/s3_13-enable-logging#terraform" + "CLI": "aws s3api put-bucket-logging --bucket --bucket-logging-status '{\"LoggingEnabled\":{\"TargetBucket\":\"\",\"TargetPrefix\":\"logs/\"}}'", + "NativeIaC": "```yaml\nResources:\n :\n Type: AWS::S3::Bucket\n Properties:\n LoggingConfiguration:\n DestinationBucketName: # CRITICAL: Enables server access logging by sending logs to this bucket\n```", + "Other": "1. Open the AWS Management Console and go to S3\n2. Select the bucket with the finding\n3. Go to the Properties tab\n4. In Server access logging, click Edit\n5. Toggle Enable, choose the target log bucket, and Save", + "Terraform": "```hcl\nresource \"aws_s3_bucket_logging\" \"\" {\n bucket = \"\"\n target_bucket = \"\" # CRITICAL: Enables server access logging by specifying the target bucket\n}\n```" }, "Recommendation": { - "Text": "Ensure that S3 buckets have Logging enabled. CloudTrail data events can be used in place of S3 bucket logging. If that is the case, this finding can be considered a false positive.", - "Url": "https://docs.aws.amazon.com/AmazonS3/latest/userguide/security-best-practices.html" + "Text": "Enable **server access logging** and send logs to a dedicated log bucket with least privilege, retention, and monitoring. Complement with **CloudTrail data events** for object-level visibility. Apply **defense in depth** by centralizing logs and protecting them from tampering.", + "Url": "https://hub.prowler.com/check/s3_bucket_server_access_logging_enabled" } }, "Categories": [ + "logging", "forensics-ready" ], "DependsOn": [], diff --git a/prowler/providers/aws/services/s3/s3_bucket_shadow_resource_vulnerability/s3_bucket_shadow_resource_vulnerability.metadata.json b/prowler/providers/aws/services/s3/s3_bucket_shadow_resource_vulnerability/s3_bucket_shadow_resource_vulnerability.metadata.json index eddcb674c4..c57026a426 100644 --- a/prowler/providers/aws/services/s3/s3_bucket_shadow_resource_vulnerability/s3_bucket_shadow_resource_vulnerability.metadata.json +++ b/prowler/providers/aws/services/s3/s3_bucket_shadow_resource_vulnerability/s3_bucket_shadow_resource_vulnerability.metadata.json @@ -1,29 +1,36 @@ { "Provider": "aws", "CheckID": "s3_bucket_shadow_resource_vulnerability", - "CheckTitle": "Check for S3 buckets vulnerable to Shadow Resource Hijacking (Bucket Monopoly)", + "CheckTitle": "S3 bucket is not a known shadow resource owned by another account", "CheckType": [ - "Effects/Data Exposure" + "Software and Configuration Checks/AWS Security Best Practices", + "Effects/Data Exposure", + "Effects/Data Exfiltration", + "TTPs/Collection" ], "ServiceName": "s3", "SubServiceName": "", - "ResourceIdTemplate": "arn:partition:s3:::bucket_name", + "ResourceIdTemplate": "", "Severity": "high", "ResourceType": "AwsS3Bucket", "ResourceGroup": "storage", - "Description": "Checks for S3 buckets with predictable names that could be hijacked by an attacker before legitimate use, leading to data leakage or other security breaches.", - "Risk": "An attacker can pre-create S3 buckets with predictable names used by various AWS services. When a legitimate user's service attempts to use that bucket, it may inadvertently write sensitive data to the attacker-controlled bucket, leading to information disclosure, denial of service, or even remote code execution.", - "RelatedUrl": "https://www.aquasec.com/blog/bucket-monopoly-breaching-aws-accounts-through-shadow-resources/", + "Description": "**Amazon S3 buckets** using **predictable service naming** (e.g., `aws-glue-assets--`, `sagemaker--`) are identified and their **ownership** checked.\n\nBuckets tied to your account that match these patterns but are owned by another account-across regions-are surfaced as shadow-resource candidates.", + "Risk": "**Preclaimed buckets** matching your account's patterns let outsiders intercept service artifacts, causing:\n- Loss of **confidentiality** (templates, data exfiltration)\n- Compromised **integrity** (script/config injection RCE, privilege escalation)\n- Reduced **availability** (creation failures or redirected writes)", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://www.techtarget.com/searchsecurity/news/366602412/Researchers-unveil-AWS-vulnerabilities-shadow-resource-vector", + "https://www.aquasec.com/blog/bucket-monopoly-breaching-aws-accounts-through-shadow-resources/#section-10" + ], "Remediation": { "Code": { "CLI": "", - "NativeIaC": "", - "Other": "Manually verify the ownership of any flagged S3 buckets. If a bucket is not owned by your account, investigate its origin and purpose. If it is not a legitimate resource, you should avoid using services that may interact with it.", - "Terraform": "" + "NativeIaC": "```yaml\n# CloudFormation: pre-claim the predictable S3 bucket name to ensure your account owns it\nResources:\n :\n Type: AWS::S3::Bucket\n Properties:\n BucketName: # Critical: create the exact bucket name reported by the check so it is owned by this account\n```", + "Other": "1. In the Prowler finding, copy the exact bucket name flagged (e.g., aws-glue-assets--).\n2. Open the AWS S3 console and click Create bucket.\n3. Paste the exact bucket name and select the region that matches the name.\n4. Click Create bucket.\n5. Repeat for each flagged name/region so all predictable service buckets are owned by your account.", + "Terraform": "```hcl\n# Terraform: pre-claim the predictable S3 bucket name to ensure your account owns it\nresource \"aws_s3_bucket\" \"\" {\n bucket = \"\" # Critical: create the exact bucket name reported by the check so it is owned by this account\n}\n```" }, "Recommendation": { - "Text": "Ensure that all S3 buckets associated with your AWS account are owned by your account. Be cautious of services that create buckets with predictable names. Whenever possible, pre-create these buckets in all regions to prevent hijacking.", - "Url": "https://www.aquasec.com/blog/bucket-monopoly-breaching-aws-accounts-through-shadow-resources/" + "Text": "Apply **defense in depth**:\n- **Preprovision and own** required service buckets in all current and planned regions\n- Enforce **least privilege** so services write only to approved bucket names/ARNs\n- Use **non-guessable names** where you control naming\n- Monitor for look-alike buckets and separate duties for bucket creation vs. use", + "Url": "https://hub.prowler.com/check/s3_bucket_shadow_resource_vulnerability" } }, "Categories": [ diff --git a/prowler/providers/aws/services/s3/s3_multi_region_access_point_public_access_block/s3_multi_region_access_point_public_access_block.metadata.json b/prowler/providers/aws/services/s3/s3_multi_region_access_point_public_access_block/s3_multi_region_access_point_public_access_block.metadata.json index 8a8d7e1f59..6f3d07ed9c 100644 --- a/prowler/providers/aws/services/s3/s3_multi_region_access_point_public_access_block/s3_multi_region_access_point_public_access_block.metadata.json +++ b/prowler/providers/aws/services/s3/s3_multi_region_access_point_public_access_block/s3_multi_region_access_point_public_access_block.metadata.json @@ -1,32 +1,41 @@ { "Provider": "aws", "CheckID": "s3_multi_region_access_point_public_access_block", - "CheckTitle": "Block Public Access Settings enabled on Multi Region Access Points.", + "CheckTitle": "S3 Multi-Region Access Point has all Block Public Access settings enabled", "CheckType": [ - "Software and Configuration Checks/Industry and Regulatory Standards/AWS Foundational Security Best Practices" + "Software and Configuration Checks/AWS Security Best Practices/Network Reachability", + "Software and Configuration Checks/Industry and Regulatory Standards/AWS Foundational Security Best Practices", + "Effects/Data Exposure" ], "ServiceName": "s3", "SubServiceName": "", - "ResourceIdTemplate": "arn:aws:s3:region:account-id:accesspoint/access-point-name", + "ResourceIdTemplate": "", "Severity": "high", "ResourceType": "AwsS3AccessPoint", "ResourceGroup": "storage", - "Description": "Ensures that public access is blocked on S3 Access Points.", - "Risk": "Leaving S3 multi region access points open to the public in AWS can lead to data exposure, breaches, compliance violations, unauthorized access, and data integrity issues.", - "RelatedUrl": "https://aws.amazon.com/es/getting-started/hands-on/getting-started-with-amazon-s3-multi-region-access-points/", + "Description": "**Amazon S3 Multi-Region Access Points** are evaluated for **Block Public Access** being fully enabled (`block_public_acls`, `ignore_public_acls`, `block_public_policy`, `restrict_public_buckets`).\n\nFocus is on the MRAP's own settings, separate from bucket or account configurations.", + "Risk": "Without MRAP **Block Public Access**, the global endpoint can accept internet traffic, exposing linked buckets.\n\nThis undermines confidentiality (object listing/reads) and integrity (unauthorized writes or policy abuse), and can trigger costly egress and data tampering across Regions.", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://docs.aws.amazon.com/AmazonS3/latest/userguide/multi-region-access-point-block-public-access.html", + "https://aws.amazon.com/es/getting-started/hands-on/getting-started-with-amazon-s3-multi-region-access-points/", + "https://docs.aws.amazon.com/securityhub/latest/userguide/s3-controls.html#s3-24" + ], "Remediation": { "Code": { "CLI": "", - "NativeIaC": "", - "Other": "https://docs.aws.amazon.com/securityhub/latest/userguide/s3-controls.html#s3-24", - "Terraform": "" + "NativeIaC": "```yaml\n# CloudFormation: MRAP with Block Public Access enabled (cannot be changed after creation)\nResources:\n :\n Type: AWS::S3::MultiRegionAccessPoint\n Properties:\n Name: \n PublicAccessBlockConfiguration: # Critical: enable all Block Public Access settings\n BlockPublicAcls: true # Critical: blocks ACLs granting public access\n IgnorePublicAcls: true # Critical: ignores any public ACLs\n BlockPublicPolicy: true # Critical: blocks public bucket policies\n RestrictPublicBuckets: true # Critical: restricts public bucket policies\n Regions:\n - Bucket: \n - Bucket: \n```", + "Other": "1. In the AWS Console, go to S3 > Multi-Region Access Points\n2. Select the failing Multi-Region Access Point and choose Delete (settings cannot be edited after creation)\n3. Click Create Multi-Region Access Point\n4. Enter a name and select at least two buckets in different Regions\n5. Ensure Block public access is enabled for all four settings (default): BlockPublicAcls, IgnorePublicAcls, BlockPublicPolicy, RestrictPublicBuckets\n6. Create the Multi-Region Access Point", + "Terraform": "```hcl\n# Terraform: MRAP with Block Public Access enabled (must be set at creation)\nresource \"aws_s3control_multi_region_access_point\" \"\" {\n account_id = \"\"\n\n details {\n name = \"\"\n\n public_access_block { # Critical: enable all Block Public Access settings\n block_public_acls = true # Critical\n ignore_public_acls = true # Critical\n block_public_policy = true # Critical\n restrict_public_buckets = true # Critical\n }\n\n region { bucket = \"\" }\n region { bucket = \"\" }\n }\n}\n```" }, "Recommendation": { - "Text": "Ensure S3 multi region access points are private by default, applying strict access controls, and regularly auditing permissions to prevent unauthorized public access.", - "Url": "https://docs.aws.amazon.com/AmazonS3/latest/userguide/multi-region-access-point-block-public-access.html" + "Text": "Adopt **deny-by-default**: keep all MRAP **Block Public Access** settings enabled; avoid public ACLs or policies.\n- Enforce **least privilege**\n- Prefer private access (VPC endpoints)\n- Periodically review permissions and logs\n\n*MRAP public access settings are immutable after creation.*", + "Url": "https://hub.prowler.com/check/s3_multi_region_access_point_public_access_block" } }, - "Categories": [], + "Categories": [ + "internet-exposed" + ], "DependsOn": [], "RelatedTo": [], "Notes": "" diff --git a/prowler/providers/aws/services/transfer/transfer_server_in_transit_encryption_enabled/transfer_server_in_transit_encryption_enabled.metadata.json b/prowler/providers/aws/services/transfer/transfer_server_in_transit_encryption_enabled/transfer_server_in_transit_encryption_enabled.metadata.json index f7f45c8241..2cd71477a5 100644 --- a/prowler/providers/aws/services/transfer/transfer_server_in_transit_encryption_enabled/transfer_server_in_transit_encryption_enabled.metadata.json +++ b/prowler/providers/aws/services/transfer/transfer_server_in_transit_encryption_enabled/transfer_server_in_transit_encryption_enabled.metadata.json @@ -1,32 +1,44 @@ { "Provider": "aws", "CheckID": "transfer_server_in_transit_encryption_enabled", - "CheckTitle": "Transfer Family Servers should have encryption in transit enabled.", + "CheckTitle": "Transfer Family server has encryption in transit enabled", "CheckType": [ - "Software and Configuration Checks/Industry and Regulatory Standards/NIST 800-53 Controls" + "Software and Configuration Checks/AWS Security Best Practices", + "Software and Configuration Checks/Industry and Regulatory Standards/AWS Foundational Security Best Practices", + "Software and Configuration Checks/Industry and Regulatory Standards/NIST 800-53 Controls", + "Software and Configuration Checks/Industry and Regulatory Standards/PCI-DSS", + "Software and Configuration Checks/Industry and Regulatory Standards/HIPAA Controls (USA)", + "Effects/Data Exposure" ], "ServiceName": "transfer", "SubServiceName": "", - "ResourceIdTemplate": "arn:partition:transfer:region:account-id:server/server-id", - "Severity": "medium", - "ResourceType": "AwsTransferServer", + "ResourceIdTemplate": "", + "Severity": "high", + "ResourceType": "Other", "ResourceGroup": "storage", - "Description": "Ensure that your Transfer Family servers have encryption in transit enabled.", - "Risk": "Using FTP for endpoint connections leaves data in transit unencrypted, making it susceptible to interception by attackers. FTP lacks encryption, which exposes your data to person-in-the-middle and other interception risks. Adopting encrypted protocols such as SFTP, FTPS, or AS2 provides a layer of protection that helps secure sensitive data during transfer.", - "RelatedUrl": "https://docs.aws.amazon.com/config/latest/developerguide/transfer-family-server-no-ftp.html", + "Description": "**AWS Transfer Family servers** are evaluated for presence of the unencrypted `FTP` protocol among enabled protocols, as opposed to encrypted options like SFTP, FTPS, or AS2.", + "Risk": "Allowing **FTP** exposes credentials and file contents in cleartext, breaking confidentiality. Adversaries can sniff or perform **MITM** to read or alter files, compromising integrity and enabling credential theft that can be reused for broader unauthorized access.", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://docs.aws.amazon.com/config/latest/developerguide/transfer-family-server-no-ftp.html", + "https://docs.aws.amazon.com/transfer/latest/userguide/edit-server-config.html#edit-protocols", + "https://docs.aws.amazon.com/securityhub/latest/userguide/transfer-controls.html#transfer-2" + ], "Remediation": { "Code": { - "CLI": "aws transfer update-server --server-id --protocols SFTP FTPS AS2", - "NativeIaC": "", - "Other": "https://docs.aws.amazon.com/securityhub/latest/userguide/transfer-controls.html#transfer-2", - "Terraform": "" + "CLI": "aws transfer update-server --server-id --protocols SFTP", + "NativeIaC": "```yaml\n# CloudFormation: ensure FTP is not enabled\nResources:\n :\n Type: AWS::Transfer::Server\n Properties:\n Protocols:\n - SFTP # CRITICAL: Use SFTP only; excludes FTP (unencrypted)\n```", + "Other": "1. Open AWS Console > AWS Transfer Family\n2. Go to Servers and select the server ()\n3. Click Edit next to Protocols\n4. Uncheck FTP and ensure at least SFTP (or FTPS/AS2) is selected\n5. Save", + "Terraform": "```hcl\n# Ensure FTP is not enabled\nresource \"aws_transfer_server\" \"\" {\n protocols = [\"SFTP\"] # CRITICAL: Excludes FTP to enforce encryption in transit\n}\n```" }, "Recommendation": { - "Text": "Configure AWS Transfer Family servers to use secure protocols, such as SFTP, FTPS, or AS2, instead of FTP to protect data in transit. These protocols offer encryption, reducing exposure to interception and manipulation attacks.", - "Url": "https://docs.aws.amazon.com/transfer/latest/userguide/edit-server-config.html#edit-protocols" + "Text": "Remove `FTP`; permit only **SFTP**, **FTPS**, or **AS2** to enforce **encryption in transit**.\n\nApply defense in depth: restrict by network location (allowlists/VPC), enforce strong cryptographic policies, and use least-privilege roles with monitoring.", + "Url": "https://hub.prowler.com/check/transfer_server_in_transit_encryption_enabled" } }, - "Categories": [], + "Categories": [ + "encryption" + ], "DependsOn": [], "RelatedTo": [], "Notes": "" diff --git a/prowler/providers/aws/services/vpc/vpc_different_regions/vpc_different_regions.metadata.json b/prowler/providers/aws/services/vpc/vpc_different_regions/vpc_different_regions.metadata.json index 060e50a151..ce81036faa 100644 --- a/prowler/providers/aws/services/vpc/vpc_different_regions/vpc_different_regions.metadata.json +++ b/prowler/providers/aws/services/vpc/vpc_different_regions/vpc_different_regions.metadata.json @@ -1,32 +1,39 @@ { "Provider": "aws", "CheckID": "vpc_different_regions", - "CheckTitle": "Ensure there are VPCs in more than one region", + "CheckTitle": "VPCs are present in more than one region", "CheckType": [ - "Infrastructure Security" + "Software and Configuration Checks/AWS Security Best Practices", + "Effects/Denial of Service" ], "ServiceName": "vpc", - "SubServiceName": "subnet", - "ResourceIdTemplate": "arn:partition:service:region:account-id:resource-id", + "SubServiceName": "", + "ResourceIdTemplate": "", "Severity": "medium", "ResourceType": "AwsEc2Vpc", "ResourceGroup": "network", - "Description": "Ensure there are VPCs in more than one region", - "Risk": "", - "RelatedUrl": "https://docs.aws.amazon.com/vpc/latest/userguide/VPC_Scenario2.html", + "Description": "Non-default **VPCs** are evaluated across the account to determine whether they exist in **more than one region**. The result reflects if your custom network topology is regionally distributed or concentrated in a single region.", + "Risk": "Single-region VPC deployment weakens **availability** and **resilience**. A regional outage, service disruption, or network control misconfiguration can cause broad downtime, hinder recovery, and increase the **blast radius** of incidents impacting business continuity.", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://docs.aws.amazon.com/vpc/latest/userguide/vpc-example-private-subnets-nat.html", + "https://docs.aws.amazon.com/vpc/latest/userguide/VPC_Scenario2.html" + ], "Remediation": { "Code": { - "CLI": "aws ec2 create-vpc", - "NativeIaC": "", - "Other": "", - "Terraform": "" + "CLI": "aws ec2 create-vpc --region --cidr-block ", + "NativeIaC": "```yaml\n# Deploy this stack in a second AWS region to pass the check\nResources:\n :\n Type: AWS::EC2::VPC\n Properties:\n CidrBlock: 10.0.0.0/16 # Critical: creates a non-default VPC in this region\n```", + "Other": "1. Open the AWS Console and go to VPC\n2. In the Region selector (top right), choose a different region than your existing non-default VPCs\n3. Click Create VPC > VPC only\n4. Enter an IPv4 CIDR block (e.g., 10.0.0.0/16)\n5. Click Create VPC\n6. Verify a non-default VPC now exists in this second region", + "Terraform": "```hcl\nprovider \"aws\" {\n alias = \"other\"\n region = \"\" # Critical: ensures the VPC is created in a second region\n}\n\nresource \"aws_vpc\" \"\" {\n provider = aws.other\n cidr_block = \"10.0.0.0/16\" # Critical: creates a non-default VPC in that region\n}\n```" }, "Recommendation": { - "Text": "Ensure there are VPCs in more than one region", - "Url": "https://docs.aws.amazon.com/vpc/latest/userguide/vpc-example-private-subnets-nat.html" + "Text": "Adopt a **multi-region network design**:\n- Create VPCs in at least two regions for critical workloads\n- Replicate routing, security controls, and endpoints consistently\n- Apply **fault tolerance** and **defense in depth** with data replication and resilient DNS/failover to avoid single-region dependency", + "Url": "https://hub.prowler.com/check/vpc_different_regions" } }, - "Categories": [], + "Categories": [ + "resilience" + ], "DependsOn": [], "RelatedTo": [], "Notes": "" diff --git a/prowler/providers/aws/services/vpc/vpc_endpoint_connections_trust_boundaries/vpc_endpoint_connections_trust_boundaries.metadata.json b/prowler/providers/aws/services/vpc/vpc_endpoint_connections_trust_boundaries/vpc_endpoint_connections_trust_boundaries.metadata.json index 7885555148..9460f24bc1 100644 --- a/prowler/providers/aws/services/vpc/vpc_endpoint_connections_trust_boundaries/vpc_endpoint_connections_trust_boundaries.metadata.json +++ b/prowler/providers/aws/services/vpc/vpc_endpoint_connections_trust_boundaries/vpc_endpoint_connections_trust_boundaries.metadata.json @@ -1,33 +1,39 @@ { "Provider": "aws", "CheckID": "vpc_endpoint_connections_trust_boundaries", - "CheckTitle": "Find trust boundaries in VPC endpoint connections.", + "CheckTitle": "VPC endpoint policy allows access only from trusted AWS accounts", "CheckType": [ - "Infrastructure Security" + "Software and Configuration Checks/AWS Security Best Practices", + "Industry and Regulatory Standards/AWS Foundational Security Best Practices", + "TTPs/Initial Access" ], "ServiceName": "vpc", - "SubServiceName": "endpoint", - "ResourceIdTemplate": "arn:partition:service:region:account-id:resource-id", - "Severity": "medium", + "SubServiceName": "", + "ResourceIdTemplate": "", + "Severity": "high", "ResourceType": "AwsEc2VpcEndpointService", "ResourceGroup": "network", - "Description": "Find trust boundaries in VPC endpoint connections.", - "Risk": "Account VPC could be linked to other accounts.", + "Description": "**VPC endpoint policies** are assessed for restriction to configured **trusted AWS accounts**. If `Principal` values (including `*`) or account ARNs permit non-trusted principals, or conditions aren't sufficiently restrictive, the endpoint is identified. *Endpoints without editable policies are excluded.*", + "Risk": "Non-trusted principals using your endpoint can access AWS services as if from your VPC, weakening segmentation. This enables unauthorized reads/writes and data exfiltration from resources tied to the endpoint, harming **confidentiality** and **integrity**, and potentially increasing **costs**.", "RelatedUrl": "", + "AdditionalURLs": [ + "https://docs.aws.amazon.com/vpc/latest/privatelink/vpc-endpoints-access.html" + ], "Remediation": { "Code": { - "CLI": "", - "NativeIaC": "", - "Other": "https://docs.prowler.com/checks/aws/networking-policies/networking_9#aws-vpc-endpoints-are-exposed", - "Terraform": "" + "CLI": "aws ec2 modify-vpc-endpoint --vpc-endpoint-id --policy-document '{\"Version\":\"2012-10-17\",\"Statement\":[{\"Effect\":\"Allow\",\"Principal\":\"*\",\"Action\":\"*\",\"Resource\":\"*\",\"Condition\":{\"StringEquals\":{\"aws:PrincipalAccount\":[\"\",\"\"]}}}]}'", + "NativeIaC": "```yaml\n# CloudFormation: restrict VPC endpoint access to trusted accounts only\nResources:\n :\n Type: AWS::EC2::VPCEndpoint\n Properties:\n VpcId: \n ServiceName: com.amazonaws..\n PolicyDocument:\n Version: '2012-10-17'\n Statement:\n - Effect: Allow\n Principal: \"*\"\n Action: \"*\"\n Resource: \"*\"\n Condition: # CRITICAL: restrict by trusted accounts\n StringEquals: # CRITICAL: only allow specified accounts\n \"aws:PrincipalAccount\": # CRITICAL: limits usage to these accounts\n - \"\"\n - \"\"\n```", + "Other": "1. Open the AWS Console and go to VPC > Endpoints\n2. Select the endpoint and choose Actions > Manage policy\n3. Select Custom and paste this minimal policy, replacing with your trusted account IDs:\n ```json\n {\n \"Version\": \"2012-10-17\",\n \"Statement\": [\n {\n \"Effect\": \"Allow\",\n \"Principal\": \"*\",\n \"Action\": \"*\",\n \"Resource\": \"*\",\n \"Condition\": {\n \"StringEquals\": {\n \"aws:PrincipalAccount\": [\n \"\",\n \"\"\n ]\n }\n }\n }\n ]\n }\n ```\n4. Click Save", + "Terraform": "```hcl\n# Add this to the existing VPC endpoint resource to restrict access\nresource \"aws_vpc_endpoint\" \"\" {\n # ...existing required arguments for the endpoint...\n\n policy = jsonencode({\n Version = \"2012-10-17\"\n Statement = [{\n Effect = \"Allow\"\n Principal = \"*\"\n Action = \"*\"\n Resource = \"*\"\n Condition = { # CRITICAL: restrict by trusted accounts\n StringEquals = {\n \"aws:PrincipalAccount\" = [ # CRITICAL: only these accounts can use the endpoint\n \"\",\n \"\"\n ]\n }\n }\n }]\n })\n}\n```" }, "Recommendation": { - "Text": "In multi Account environments identify untrusted links. Check trust chaining and dependencies between accounts.", - "Url": "https://docs.aws.amazon.com/vpc/latest/privatelink/vpc-endpoints-access.html" + "Text": "Apply **least privilege**: restrict endpoint policies to your account and an explicit allowlist of **trusted accounts**. Avoid `*` principals unless coupled with strict conditions. Prevent transitive trust across network links, and use resource policies and monitoring as **defense in depth** to limit endpoint use.", + "Url": "https://hub.prowler.com/check/vpc_endpoint_connections_trust_boundaries" } }, "Categories": [ - "trust-boundaries" + "trust-boundaries", + "identity-access" ], "DependsOn": [], "RelatedTo": [], diff --git a/prowler/providers/aws/services/vpc/vpc_endpoint_for_ec2_enabled/vpc_endpoint_for_ec2_enabled.metadata.json b/prowler/providers/aws/services/vpc/vpc_endpoint_for_ec2_enabled/vpc_endpoint_for_ec2_enabled.metadata.json index c01abcab88..31c8058187 100644 --- a/prowler/providers/aws/services/vpc/vpc_endpoint_for_ec2_enabled/vpc_endpoint_for_ec2_enabled.metadata.json +++ b/prowler/providers/aws/services/vpc/vpc_endpoint_for_ec2_enabled/vpc_endpoint_for_ec2_enabled.metadata.json @@ -1,30 +1,40 @@ { "Provider": "aws", "CheckID": "vpc_endpoint_for_ec2_enabled", - "CheckTitle": "Amazon EC2 should be configured to use VPC endpoints that are created for the Amazon EC2 service.", - "CheckType": [], + "CheckTitle": "VPC has an Amazon EC2 VPC endpoint", + "CheckType": [ + "Software and Configuration Checks/AWS Security Best Practices/Network Reachability" + ], "ServiceName": "vpc", "SubServiceName": "", - "ResourceIdTemplate": "arn:partition:service:region:account-id:resource-id", + "ResourceIdTemplate": "", "Severity": "medium", "ResourceType": "AwsEc2VpcEndpointService", "ResourceGroup": "network", - "Description": "Ensure that a service endpoint for Amazon EC2 is created for each VPC. The check fails if a VPC does not have a VPC endpoint created for the Amazon EC2 service.", - "Risk": "Without VPC endpoints, network traffic between your VPC and Amazon EC2 may traverse the public internet, increasing the risk of unintended access or data exposure.", - "RelatedUrl": "https://docs.aws.amazon.com/config/latest/developerguide/service-vpc-endpoint-enabled.html", + "Description": "**Amazon VPCs** are evaluated for an **interface VPC endpoint** to the **Amazon EC2 API** (`ec2`). Its presence indicates private EC2 API connectivity over **AWS PrivateLink** within the VPC.", + "Risk": "Without a private EC2 endpoint, EC2 API traffic exits via IGW/NAT. This expands exposure to network path threats (e.g., DNS hijack, MITM) and weakens egress isolation. It also adds an internet egress dependency for API access, reducing availability if NAT/edge paths fail.", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://docs.aws.amazon.com/config/latest/developerguide/service-vpc-endpoint-enabled.html", + "https://docs.aws.amazon.com/securityhub/latest/userguide/ec2-controls.html#ec2-10", + "https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/interface-vpc-endpoints.html" + ], "Remediation": { "Code": { - "CLI": "", - "NativeIaC": "", - "Other": "https://docs.aws.amazon.com/securityhub/latest/userguide/ec2-controls.html#ec2-10", - "Terraform": "" + "CLI": "aws ec2 create-vpc-endpoint --vpc-id --service-name com.amazonaws..ec2 --vpc-endpoint-type Interface --subnet-ids ", + "NativeIaC": "```yaml\n# CloudFormation: create an EC2 interface VPC endpoint in the VPC\nResources:\n VPCEndpoint:\n Type: AWS::EC2::VPCEndpoint\n Properties:\n VpcId: \"\" # CRITICAL: target VPC for the endpoint\n ServiceName: !Sub \"com.amazonaws.${AWS::Region}.ec2\" # CRITICAL: EC2 interface endpoint service\n VpcEndpointType: Interface # CRITICAL: create an interface endpoint\n SubnetIds: # CRITICAL: subnets to place the endpoint ENIs\n - \"\"\n```", + "Other": "1. In the AWS console, go to VPC > Endpoints\n2. Click Create endpoint\n3. For Service category, choose AWS services and select Service name com.amazonaws..ec2\n4. Select your VPC and at least one subnet\n5. Click Create endpoint", + "Terraform": "```hcl\n# Create an EC2 interface VPC endpoint\nresource \"aws_vpc_endpoint\" \"\" {\n vpc_id = \"\" # CRITICAL: target VPC\n service_name = \"com.amazonaws..ec2\" # CRITICAL: EC2 interface endpoint service\n vpc_endpoint_type = \"Interface\" # CRITICAL: interface endpoint\n subnet_ids = [\"\"] # CRITICAL: subnet(s) for endpoint ENIs\n}\n```" }, "Recommendation": { - "Text": "To improve the security posture of your VPC, configure Amazon EC2 to use an interface VPC endpoint powered by AWS PrivateLink.", - "Url": "https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/interface-vpc-endpoints.html" + "Text": "Use an **interface VPC endpoint** for the EC2 service in each VPC that requires EC2 API access.\n- Enable **private DNS** to keep calls on the AWS network\n- Apply restrictive endpoint policies (least privilege)\n- Reduce reliance on public egress and layer controls for **defense in depth**", + "Url": "https://hub.prowler.com/check/vpc_endpoint_for_ec2_enabled" } }, - "Categories": [], + "Categories": [ + "internet-exposed", + "trust-boundaries" + ], "DependsOn": [], "RelatedTo": [], "Notes": "" diff --git a/prowler/providers/aws/services/vpc/vpc_endpoint_multi_az_enabled/vpc_endpoint_multi_az_enabled.metadata.json b/prowler/providers/aws/services/vpc/vpc_endpoint_multi_az_enabled/vpc_endpoint_multi_az_enabled.metadata.json index 10458a17cd..54cb321ecf 100644 --- a/prowler/providers/aws/services/vpc/vpc_endpoint_multi_az_enabled/vpc_endpoint_multi_az_enabled.metadata.json +++ b/prowler/providers/aws/services/vpc/vpc_endpoint_multi_az_enabled/vpc_endpoint_multi_az_enabled.metadata.json @@ -1,31 +1,38 @@ { "Provider": "aws", "CheckID": "vpc_endpoint_multi_az_enabled", - "CheckTitle": "Amazon VPC Interface Endpoints should have ENIs in more than one subnet.", - "CheckType": [], + "CheckTitle": "Amazon VPC interface endpoint has subnets in multiple Availability Zones", + "CheckType": [ + "Software and Configuration Checks/AWS Security Best Practices", + "Software and Configuration Checks/Industry and Regulatory Standards/AWS Foundational Security Best Practices" + ], "ServiceName": "vpc", "SubServiceName": "", - "ResourceIdTemplate": "arn:partition:service:region:account-id:resource-id", + "ResourceIdTemplate": "", "Severity": "medium", - "ResourceType": "AwsVpcEndpointService", + "ResourceType": "AwsEc2VpcEndpointService", "ResourceGroup": "network", - "Description": "Ensure that all vpc interface endpoints have ENIs in multiple subnets. If a VPC endpoint has an ENI in only a single subnet then this check will fail. You cannot create VPC Endpoints in 2 different subnets in the same AZ. So, for the purpose of VPC endpoints, having multiple subnets implies multiple AZs.", - "Risk": "Without VPC endpoints ENIs in multiple subnets an AZ impacting event could lead to increased downtime or your network traffic between your VPC and Amazon services may traverse the public internet.", - "RelatedUrl": "https://docs.aws.amazon.com/vpc/latest/privatelink/interface-endpoints.html", + "Description": "**VPC interface endpoints** are evaluated for whether their endpoint network interfaces are placed in **multiple subnets**, which implies distribution across different **Availability Zones**. Endpoints present in only one subnet are identified.", + "Risk": "A **single-subnet endpoint** creates a **single-AZ dependency**. An AZ outage or routing issue can cut access to the service, reducing **availability**. Workloads may revert to **public endpoints**, exposing traffic to the Internet and risking **confidentiality** through interception or tampering.", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://docs.aws.amazon.com/vpc/latest/privatelink/interface-endpoints.html", + "https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/interface-vpc-endpoints.html" + ], "Remediation": { "Code": { - "CLI": "", - "NativeIaC": "", - "Other": "", - "Terraform": "" + "CLI": "aws ec2 modify-vpc-endpoint --vpc-endpoint-id --add-subnet-ids ", + "NativeIaC": "```yaml\nResources:\n :\n Type: AWS::EC2::VPCEndpoint\n Properties:\n VpcEndpointType: Interface\n VpcId: \n ServiceName: com.amazonaws..\n SubnetIds: # CRITICAL: include at least two subnets (preferably in different AZs) to pass the check\n - \n - \n```", + "Other": "1. Open the AWS VPC console and go to Endpoints\n2. Select the interface endpoint\n3. Click Actions > Manage subnets\n4. Select an additional subnet in a different Availability Zone\n5. Click Modify subnets to save", + "Terraform": "```hcl\nresource \"aws_vpc_endpoint\" \"\" {\n vpc_id = \"\"\n service_name = \"com.amazonaws..\"\n vpc_endpoint_type = \"Interface\"\n\n subnet_ids = [\n \"\",\n \"\" # CRITICAL: add a second subnet (ideally in a different AZ) to satisfy multi-AZ\n ]\n}\n```" }, "Recommendation": { - "Text": "To improve the availability of your services residing in your VPC, configure multiple subnets for VPC Interface Endpoints.", - "Url": "https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/interface-vpc-endpoints.html" + "Text": "Place interface endpoints in **multiple subnets across distinct AZs** to remove single-AZ reliance. Prefer zone-local routing so clients use the nearest endpoint, and combine with **private DNS** and restrictive **security groups** to limit exposure-supporting **defense in depth** and resilient connectivity.", + "Url": "https://hub.prowler.com/check/vpc_endpoint_multi_az_enabled" } }, "Categories": [ - "redundancy" + "resilience" ], "DependsOn": [], "RelatedTo": [], diff --git a/prowler/providers/aws/services/vpc/vpc_endpoint_services_allowed_principals_trust_boundaries/vpc_endpoint_services_allowed_principals_trust_boundaries.metadata.json b/prowler/providers/aws/services/vpc/vpc_endpoint_services_allowed_principals_trust_boundaries/vpc_endpoint_services_allowed_principals_trust_boundaries.metadata.json index b1e0bd5b8b..fe58f3c991 100644 --- a/prowler/providers/aws/services/vpc/vpc_endpoint_services_allowed_principals_trust_boundaries/vpc_endpoint_services_allowed_principals_trust_boundaries.metadata.json +++ b/prowler/providers/aws/services/vpc/vpc_endpoint_services_allowed_principals_trust_boundaries/vpc_endpoint_services_allowed_principals_trust_boundaries.metadata.json @@ -1,29 +1,35 @@ { "Provider": "aws", "CheckID": "vpc_endpoint_services_allowed_principals_trust_boundaries", - "CheckTitle": "Find trust boundaries in VPC endpoint services allowlisted principles.", + "CheckTitle": "VPC endpoint service allows only trusted principals or none", "CheckType": [ - "Infrastructure Security" + "Software and Configuration Checks/AWS Security Best Practices/Network Reachability", + "Software and Configuration Checks/Industry and Regulatory Standards/AWS Foundational Security Best Practices", + "TTPs/Initial Access", + "Effects/Data Exposure" ], "ServiceName": "vpc", - "SubServiceName": "service_endpoint", - "ResourceIdTemplate": "arn:partition:service:region:account-id:resource-id", - "Severity": "medium", + "SubServiceName": "", + "ResourceIdTemplate": "", + "Severity": "high", "ResourceType": "AwsEc2VpcEndpointService", "ResourceGroup": "network", - "Description": "Find trust boundaries in VPC endpoint services allowlisted principles.", - "Risk": "Account VPC could be linked to other accounts.", + "Description": "**VPC endpoint services** are assessed for their **allowed principals**, comparing each to a configured set of trusted accounts and identifying any **untrusted principals** or a wildcard `*` present in the allowlist.", + "Risk": "Untrusted or wildcard principals can create PrivateLink connections to your service, eroding segmentation. This enables unauthorized data access (**confidentiality**), abuse of internal APIs (**integrity**), and excess load on backends (**availability**).", "RelatedUrl": "", + "AdditionalURLs": [ + "https://docs.aws.amazon.com/vpc/latest/privatelink/vpc-endpoints-access.html" + ], "Remediation": { "Code": { "CLI": "", - "NativeIaC": "", - "Other": "https://docs.prowler.com/checks/aws/networking-policies/networking_9#aws-vpc-endpoints-are-exposed", - "Terraform": "" + "NativeIaC": "```yaml\n# Allow only a trusted principal (or attach none to allow no principals)\nResources:\n :\n Type: AWS::EC2::VPCEndpointServicePermissions\n Properties:\n ServiceId: \n AllowedPrincipals:\n - arn:aws:iam:::root # CRITICAL: restricts access to only this trusted account\n```", + "Other": "1. Open the AWS VPC console and go to Endpoint services\n2. Select the endpoint service ()\n3. Open the Allowed principals tab and click Edit allowed principals\n4. Remove all entries that are not trusted, including any wildcard (*)\n5. Optionally leave the list empty (no principals) or keep only trusted account IDs/ARNs\n6. Save changes", + "Terraform": "```hcl\n# Allow only a trusted principal (delete this resource to allow none)\nresource \"aws_vpc_endpoint_service_allowed_principal\" \"\" {\n service_id = \"\"\n principal_arn = \"arn:aws:iam:::root\" # CRITICAL: only this trusted account can create endpoints\n}\n```" }, "Recommendation": { - "Text": "In multi Account environments identify untrusted links. Check trust chaining and dependencies between accounts.", - "Url": "https://docs.aws.amazon.com/vpc/latest/privatelink/vpc-endpoints-access.html" + "Text": "Apply **least privilege**: restrict **allowed principals** to vetted account IDs and avoid `*`. Maintain a central trust registry, enforce **separation of duties** with approval workflows, and review entries regularly. Use **defense in depth** with strong service authentication and continuous configuration monitoring.", + "Url": "https://hub.prowler.com/check/vpc_endpoint_services_allowed_principals_trust_boundaries" } }, "Categories": [ diff --git a/prowler/providers/aws/services/vpc/vpc_flow_logs_enabled/vpc_flow_logs_enabled.metadata.json b/prowler/providers/aws/services/vpc/vpc_flow_logs_enabled/vpc_flow_logs_enabled.metadata.json index a773a2e654..114bc78956 100644 --- a/prowler/providers/aws/services/vpc/vpc_flow_logs_enabled/vpc_flow_logs_enabled.metadata.json +++ b/prowler/providers/aws/services/vpc/vpc_flow_logs_enabled/vpc_flow_logs_enabled.metadata.json @@ -1,34 +1,43 @@ { "Provider": "aws", "CheckID": "vpc_flow_logs_enabled", - "CheckTitle": "Ensure VPC Flow Logging is Enabled in all VPCs.", + "CheckTitle": "VPC flow logs are enabled", "CheckType": [ - "Logging and Monitoring" + "Software and Configuration Checks/AWS Security Best Practices", + "Software and Configuration Checks/Industry and Regulatory Standards/AWS Foundational Security Best Practices", + "Software and Configuration Checks/Industry and Regulatory Standards/CIS AWS Foundations Benchmark", + "Software and Configuration Checks/Industry and Regulatory Standards/PCI-DSS", + "Software and Configuration Checks/Industry and Regulatory Standards/NIST 800-53 Controls (USA)", + "Software and Configuration Checks/Industry and Regulatory Standards/NIST CSF Controls (USA)" ], "ServiceName": "vpc", - "SubServiceName": "flow_log", - "ResourceIdTemplate": "arn:partition:service:region:account-id:resource-id", + "SubServiceName": "", + "ResourceIdTemplate": "", "Severity": "medium", "ResourceType": "AwsEc2Vpc", "ResourceGroup": "network", - "Description": "Ensure VPC Flow Logging is Enabled in all VPCs.", - "Risk": "VPC Flow Logs provide visibility into network traffic that traverses the VPC and can be used to detect anomalous traffic or insight during security workflows.", + "Description": "**AWS VPCs** have **Flow Logs** configured to capture IP traffic for their network interfaces and deliver records to a logging destination.\n\nVPCs lacking an active flow log configuration are highlighted.", + "Risk": "Without flow logs, network activity is opaque, hindering detection and investigation of malicious traffic. Attackers can probe, exfiltrate, or move laterally unnoticed, impacting **confidentiality** and **integrity**; outages and misconfigurations are harder to diagnose, reducing **availability**.", "RelatedUrl": "", + "AdditionalURLs": [ + "http://docs.aws.amazon.com/AmazonVPC/latest/UserGuide/flow-logs.html", + "https://www.trendmicro.com/cloudoneconformity/knowledge-base/aws/VPC/vpc-flow-logs-enabled.html" + ], "Remediation": { "Code": { - "CLI": "", - "NativeIaC": "", - "Other": "https://docs.prowler.com/checks/aws/logging-policies/logging_9-enable-vpc-flow-logging#aws-console", - "Terraform": "https://docs.prowler.com/checks/aws/logging-policies/logging_9-enable-vpc-flow-logging#terraform" + "CLI": "aws ec2 create-flow-logs --resource-type VPC --resource-ids --traffic-type ALL --log-destination-type s3 --log-destination arn:aws:s3:::", + "NativeIaC": "```yaml\n# CloudFormation: Enable VPC Flow Logs to S3 for an existing VPC\nResources:\n FlowLog:\n Type: AWS::EC2::FlowLog\n Properties:\n ResourceId: # Critical: target the VPC ID\n ResourceType: VPC # Critical: enable flow logs at VPC level\n TrafficType: ALL # Critical: log all traffic\n LogDestinationType: s3 # Critical: send logs to S3 (no IAM role needed)\n LogDestination: arn:aws:s3::: # Critical: S3 bucket ARN\n```", + "Other": "1. In the AWS Console, go to VPC > Your VPCs\n2. Select the target VPC\n3. Open the Flow logs tab and click Create flow log\n4. Set Traffic type to All\n5. Set Destination to S3 and enter Bucket ARN: arn:aws:s3:::\n6. Click Create flow log", + "Terraform": "```hcl\n# Enable VPC Flow Logs to S3 for an existing VPC\nresource \"aws_flow_log\" \"vpc\" {\n vpc_id = \"\" # Critical: target the VPC to enable flow logs\n traffic_type = \"ALL\" # Critical: log all traffic\n log_destination_type = \"s3\" # Critical: send logs to S3 (no IAM role needed)\n log_destination = \"arn:aws:s3:::\" # Critical: S3 bucket ARN\n}\n```" }, "Recommendation": { - "Text": "It is recommended that VPC Flow Logs be enabled for packet Rejects for VPCs.", - "Url": "http://docs.aws.amazon.com/AmazonVPC/latest/UserGuide/flow-logs.html" + "Text": "Enable **VPC Flow Logs** for all VPCs to provide baseline telemetry.\nPrefer capturing at least `REJECT` and, for sensitive networks, `ALL`. Send logs to a centralized, access-controlled destination with retention. Apply **least privilege** to writers/readers and integrate with monitoring for **defense in depth**.", + "Url": "https://hub.prowler.com/check/vpc_flow_logs_enabled" } }, "Categories": [ - "forensics-ready", - "logging" + "logging", + "forensics-ready" ], "DependsOn": [], "RelatedTo": [], diff --git a/prowler/providers/aws/services/vpc/vpc_peering_routing_tables_with_least_privilege/vpc_peering_routing_tables_with_least_privilege.metadata.json b/prowler/providers/aws/services/vpc/vpc_peering_routing_tables_with_least_privilege/vpc_peering_routing_tables_with_least_privilege.metadata.json index 46579d5a47..7bfc609ad6 100644 --- a/prowler/providers/aws/services/vpc/vpc_peering_routing_tables_with_least_privilege/vpc_peering_routing_tables_with_least_privilege.metadata.json +++ b/prowler/providers/aws/services/vpc/vpc_peering_routing_tables_with_least_privilege/vpc_peering_routing_tables_with_least_privilege.metadata.json @@ -1,32 +1,41 @@ { "Provider": "aws", "CheckID": "vpc_peering_routing_tables_with_least_privilege", - "CheckTitle": "Ensure routing tables for VPC peering are least access.", + "CheckTitle": "VPC peering connection route tables do not include 0.0.0.0/0 or entire requester/accepter VPC CIDR routes", "CheckType": [ - "Infrastructure Security" + "Software and Configuration Checks/AWS Security Best Practices/Network Reachability", + "Software and Configuration Checks/Industry and Regulatory Standards/AWS Foundational Security Best Practices", + "TTPs/Lateral Movement", + "Effects/Data Exposure" ], "ServiceName": "vpc", - "SubServiceName": "route_table", - "ResourceIdTemplate": "arn:partition:service:region:account-id:resource-id", + "SubServiceName": "", + "ResourceIdTemplate": "", "Severity": "medium", "ResourceType": "AwsEc2VpcPeeringConnection", "ResourceGroup": "network", - "Description": "Ensure routing tables for VPC peering are least access.", - "Risk": "Being highly selective in peering routing tables is a very effective way of minimizing the impact of breach as resources outside of these routes are inaccessible to the peered VPC.", - "RelatedUrl": "https://www.trendmicro.com/cloudoneconformity-staging/knowledge-base/aws/VPC/vpc-peering-access.html#", + "Description": "**AWS VPC peering** route tables are assessed for **least-privilege routing**. Routes that target `0.0.0.0/0` or an entire peer VPC CIDR are considered overly broad; only specific subnets or narrower prefixes should be advertised across the peering link.", + "Risk": "Broad peering routes expand cross-VPC reachability, enabling lateral movement and unauthorized discovery, harming **confidentiality** and **integrity**. Using `0.0.0.0/0` or full-CIDR paths also increases misrouting with overlapping ranges, reducing network **availability**.", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://www.trendmicro.com/cloudoneconformity-staging/knowledge-base/aws/VPC/vpc-peering-access.html#", + "https://docs.aws.amazon.com/vpc/latest/peering/peering-configurations-partial-access.html" + ], "Remediation": { "Code": { "CLI": "", - "NativeIaC": "", - "Other": "https://docs.prowler.com/checks/aws/networking-policies/networking_5", - "Terraform": "" + "NativeIaC": "```yaml\n# CloudFormation: restrict VPC peering route to a specific subnet (least privilege)\nResources:\n PeeringSpecificRoute:\n Type: AWS::EC2::Route\n Properties:\n RouteTableId: \n DestinationCidrBlock: 10.0.1.0/24 # CRITICAL: use a specific subnet, not 0.0.0.0/0 or full VPC CIDR\n VpcPeeringConnectionId: \n```", + "Other": "1. In AWS Console, go to VPC > Route tables\n2. Select the route table(s) used to reach the peered VPC and click Routes > Edit routes\n3. Delete any route to 0.0.0.0/0 or to the entire requester/accepter VPC CIDR that targets the VPC peering connection\n4. Add route(s) only to required subnet CIDR(s) in the peer VPC, targeting the same VPC peering connection\n5. Save changes and repeat in the peer VPC's route table(s)", + "Terraform": "```hcl\n# Restrict VPC peering route to a specific subnet (least privilege)\nresource \"aws_route\" \"peering_specific\" {\n route_table_id = \"\"\n destination_cidr_block = \"10.0.1.0/24\" # CRITICAL: specific subnet, not 0.0.0.0/0 or full VPC CIDR\n vpc_peering_connection_id = \"\"\n}\n```" }, "Recommendation": { - "Text": "Review routing tables of peered VPCs for whether they route all subnets of each VPC and whether that is necessary to accomplish the intended purposes for peering the VPCs.", - "Url": "https://docs.aws.amazon.com/vpc/latest/peering/peering-configurations-partial-access.html" + "Text": "Enforce **least privilege** in peering: advertise only required subnets or more specific prefixes; avoid `0.0.0.0/0` and whole VPC ranges. Keep routes symmetric on both sides, and layer **defense in depth** with security groups and NACLs. *If broad connectivity is required*, prefer segmented designs or a transit gateway.", + "Url": "https://hub.prowler.com/check/vpc_peering_routing_tables_with_least_privilege" } }, - "Categories": [], + "Categories": [ + "trust-boundaries" + ], "DependsOn": [], "RelatedTo": [], "Notes": "" diff --git a/prowler/providers/aws/services/vpc/vpc_subnet_different_az/vpc_subnet_different_az.metadata.json b/prowler/providers/aws/services/vpc/vpc_subnet_different_az/vpc_subnet_different_az.metadata.json index e49a527d3f..248e8a906a 100644 --- a/prowler/providers/aws/services/vpc/vpc_subnet_different_az/vpc_subnet_different_az.metadata.json +++ b/prowler/providers/aws/services/vpc/vpc_subnet_different_az/vpc_subnet_different_az.metadata.json @@ -1,33 +1,38 @@ { "Provider": "aws", "CheckID": "vpc_subnet_different_az", - "CheckTitle": "Ensure all VPC has subnets in more than one availability zone", + "CheckTitle": "VPC has subnets in more than one Availability Zone", "CheckType": [ - "Infrastructure Security" + "Software and Configuration Checks/AWS Security Best Practices", + "Effects/Denial of Service" ], "ServiceName": "vpc", - "SubServiceName": "subnet", - "ResourceIdTemplate": "arn:partition:service:region:account-id:resource-id", + "SubServiceName": "", + "ResourceIdTemplate": "", "Severity": "medium", - "ResourceType": "AwsEc2Vpc", + "ResourceType": "AwsEc2Subnet", "ResourceGroup": "network", - "Description": "Ensure all VPC has subnets in more than one availability zone", - "Risk": "", - "RelatedUrl": "https://docs.aws.amazon.com/vpc/latest/userguide/configure-subnets.html", + "Description": "**VPCs** are assessed for **subnets spread across multiple Availability Zones**. The finding distinguishes VPCs with subnets confined to a single AZ or with no subnets from those with subnets in `2+` distinct AZs.", + "Risk": "Single-AZ subnet layouts create a **single point of failure**, leading to **service downtime** during AZ outages, maintenance, or capacity events. Lack of **zonal redundancy** constrains load balancing and egress design, reduces **fault isolation**, and undermines availability and recovery objectives.", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://docs.aws.amazon.com/pdfs/whitepapers/latest/building-scalable-secure-multi-vpc-network-infrastructure/building-scalable-secure-multi-vpc-network-infrastructure.pdf", + "https://docs.aws.amazon.com/vpc/latest/userguide/configure-subnets.html" + ], "Remediation": { "Code": { - "CLI": "aws ec2 create-subnet", - "NativeIaC": "", - "Other": "", - "Terraform": "" + "CLI": "", + "NativeIaC": "```yaml\n# CloudFormation: create two subnets in different AZs to ensure VPC spans multiple AZs\nResources:\n SubnetA:\n Type: AWS::EC2::Subnet\n Properties:\n VpcId: \n CidrBlock: 10.0.0.0/24\n AvailabilityZone: # Critical: place subnet in AZ1 to start AZ diversity\n SubnetB:\n Type: AWS::EC2::Subnet\n Properties:\n VpcId: \n CidrBlock: 10.0.1.0/24\n AvailabilityZone: # Critical: second AZ ensures VPC has subnets in more than one AZ\n```", + "Other": "1. In the AWS Console, go to VPC > Subnets\n2. Click Create subnet\n3. Select the target VPC ()\n4. Add two subnets with non-overlapping CIDRs in different Availability Zones (e.g., and )\n5. Click Create subnet to save\n", + "Terraform": "```hcl\n# Create two subnets in different AZs so the VPC spans multiple Availability Zones\nresource \"aws_subnet\" \"subnet_a\" {\n vpc_id = \"\"\n cidr_block = \"10.0.0.0/24\"\n availability_zone = \"\" # Critical: first AZ\n}\n\nresource \"aws_subnet\" \"subnet_b\" {\n vpc_id = \"\"\n cidr_block = \"10.0.1.0/24\"\n availability_zone = \"\" # Critical: second AZ; ensures subnets in more than one AZ\n}\n```" }, "Recommendation": { - "Text": "Ensure all VPC has subnets in more than one availability zone", - "Url": "https://docs.aws.amazon.com/vpc/latest/userguide/configure-subnets.html" + "Text": "Distribute subnets across `2+` **Availability Zones** and deploy workloads in separate AZs for **high availability**. Mirror network tiers per AZ, align routing and egress per AZ, and enforce multi-AZ layouts with IaC and policy guardrails. *Regularly test failover* to validate resilience.", + "Url": "https://hub.prowler.com/check/vpc_subnet_different_az" } }, "Categories": [ - "redundancy" + "resilience" ], "DependsOn": [], "RelatedTo": [], diff --git a/prowler/providers/aws/services/vpc/vpc_subnet_no_public_ip_by_default/vpc_subnet_no_public_ip_by_default.metadata.json b/prowler/providers/aws/services/vpc/vpc_subnet_no_public_ip_by_default/vpc_subnet_no_public_ip_by_default.metadata.json index 2cc928cf3f..66be9a3ec6 100644 --- a/prowler/providers/aws/services/vpc/vpc_subnet_no_public_ip_by_default/vpc_subnet_no_public_ip_by_default.metadata.json +++ b/prowler/providers/aws/services/vpc/vpc_subnet_no_public_ip_by_default/vpc_subnet_no_public_ip_by_default.metadata.json @@ -1,32 +1,38 @@ { "Provider": "aws", "CheckID": "vpc_subnet_no_public_ip_by_default", - "CheckTitle": "Ensure VPC subnets do not assign public IP by default", + "CheckTitle": "VPC subnet does not assign public IP addresses by default", "CheckType": [ - "Infrastructure Security" + "Software and Configuration Checks/AWS Security Best Practices/Network Reachability", + "Software and Configuration Checks/Industry and Regulatory Standards/AWS Foundational Security Best Practices" ], "ServiceName": "vpc", - "SubServiceName": "subnet", - "ResourceIdTemplate": "arn:partition:service:region:account-id:resource-id", - "Severity": "medium", + "SubServiceName": "", + "ResourceIdTemplate": "", + "Severity": "high", "ResourceType": "AwsEc2Subnet", "ResourceGroup": "network", - "Description": "Ensure VPC subnets do not assign public IP by default", - "Risk": "VPC subnet is a part of the VPC having its own rules for traffic. Assigning the Public IP to the subnet automatically (on launch) can accidentally expose the instances within this subnet to internet and should be edited to 'No' post creation of the Subnet.", - "RelatedUrl": "https://docs.aws.amazon.com/config/latest/developerguide/subnet-auto-assign-public-ip-disabled.html", + "Description": "**VPC subnets** where `MapPublicIpOnLaunch` is `true` automatically assign a public IPv4 address to instances at launch.\n\nThis identifies subnets configured for default public IP assignment.", + "Risk": "**Internet-exposed instances** become reachable by default, enabling port scans, SSH/RDP brute force, and exploit attempts. Successful access can lead to data exfiltration (**confidentiality**), unauthorized changes (**integrity**), and outages (**availability**) through abuse or DDoS.", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://docs.aws.amazon.com/config/latest/developerguide/subnet-auto-assign-public-ip-disabled.html" + ], "Remediation": { "Code": { - "CLI": "", - "NativeIaC": "", - "Other": "", - "Terraform": "https://docs.prowler.com/checks/aws/networking-policies/ensure-vpc-subnets-do-not-assign-public-ip-by-default#terraform" + "CLI": "aws ec2 modify-subnet-attribute --subnet-id --no-map-public-ip-on-launch", + "NativeIaC": "```yaml\n# CloudFormation: Subnet with public IP auto-assign disabled\nResources:\n ExampleSubnet:\n Type: AWS::EC2::Subnet\n Properties:\n VpcId: \n CidrBlock: 10.0.1.0/24\n MapPublicIpOnLaunch: false # Critical: disables automatic public IPv4 assignment on instance launch\n```", + "Other": "1. Open the AWS Console and go to VPC\n2. Click Subnets and select the target subnet\n3. Choose Actions > Edit subnet settings (or Modify auto-assign IP settings)\n4. Uncheck Enable auto-assign public IPv4 address\n5. Save changes", + "Terraform": "```hcl\n# Subnet with public IP auto-assign disabled\nresource \"aws_subnet\" \"example\" {\n vpc_id = \"\"\n cidr_block = \"10.0.1.0/24\"\n\n map_public_ip_on_launch = false # Critical: prevents assigning public IPs by default\n}\n```" }, "Recommendation": { - "Text": "VPC subnets should not allow automatic public IP assignment", - "Url": "https://docs.aws.amazon.com/config/latest/developerguide/subnet-auto-assign-public-ip-disabled.html" + "Text": "Disable subnet auto-assign to enforce **least-privilege exposure**. Place workloads in **private subnets**, use controlled egress (NAT or private endpoints), and prefer bastions or SSM for administration.\n\n*When public access is necessary*, assign IPs explicitly and restrict with tight security groups and routes for **defense in depth**.", + "Url": "https://hub.prowler.com/check/vpc_subnet_no_public_ip_by_default" } }, - "Categories": [], + "Categories": [ + "internet-exposed" + ], "DependsOn": [], "RelatedTo": [], "Notes": "" diff --git a/prowler/providers/aws/services/vpc/vpc_subnet_separate_private_public/vpc_subnet_separate_private_public.metadata.json b/prowler/providers/aws/services/vpc/vpc_subnet_separate_private_public/vpc_subnet_separate_private_public.metadata.json index ee4aa914a4..47e4613ed2 100644 --- a/prowler/providers/aws/services/vpc/vpc_subnet_separate_private_public/vpc_subnet_separate_private_public.metadata.json +++ b/prowler/providers/aws/services/vpc/vpc_subnet_separate_private_public/vpc_subnet_separate_private_public.metadata.json @@ -1,32 +1,37 @@ { "Provider": "aws", "CheckID": "vpc_subnet_separate_private_public", - "CheckTitle": "Ensure all VPC has public and private subnets defined", + "CheckTitle": "VPC has both public and private subnets", "CheckType": [ - "Infrastructure Security" + "Software and Configuration Checks/AWS Security Best Practices/Network Reachability" ], "ServiceName": "vpc", - "SubServiceName": "subnet", - "ResourceIdTemplate": "arn:partition:service:region:account-id:resource-id", + "SubServiceName": "", + "ResourceIdTemplate": "", "Severity": "medium", - "ResourceType": "AwsEc2Vpc", + "ResourceType": "AwsEc2Subnet", "ResourceGroup": "network", - "Description": "Ensure all VPC has public and private subnets defined", - "Risk": "", - "RelatedUrl": "https://docs.aws.amazon.com/vpc/latest/userguide/VPC_Scenario2.html", + "Description": "**Amazon VPCs** are assessed for network segmentation: at least one **public subnet** (internet-routable) and one **private subnet** (non-internet-routable).\n\nIt flags VPCs with no subnets, only public subnets, or only private subnets.", + "Risk": "Missing subnet separation erodes **segmentation**.\n- Only public: workloads face Internet exposure, enabling scanning, brute force, and lateral movement, threatening **confidentiality** and **integrity**.\n- Only private: no controlled egress can break patching and dependencies, impacting **availability**.\n- No subnets: misconfiguration leaves services unreachable.", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://docs.aws.amazon.com/vpc/latest/userguide/VPC_Scenario2.html" + ], "Remediation": { "Code": { - "CLI": "aws ec2 create-subnet", - "NativeIaC": "", - "Other": "", - "Terraform": "" + "CLI": "", + "NativeIaC": "```yaml\n# CloudFormation: add one public and one private subnet to a VPC\nResources:\n InternetGateway:\n Type: AWS::EC2::InternetGateway\n\n VPCGatewayAttachment:\n Type: AWS::EC2::VPCGatewayAttachment\n Properties:\n VpcId: \n InternetGatewayId: !Ref InternetGateway\n\n PublicSubnet:\n Type: AWS::EC2::Subnet\n Properties:\n VpcId: \n CidrBlock: 10.0.1.0/24 # creates a subnet to be made public via route\n\n PrivateSubnet:\n Type: AWS::EC2::Subnet\n Properties:\n VpcId: \n CidrBlock: 10.0.2.0/24 # creates a subnet that remains private (no IGW route)\n\n PublicRouteTable:\n Type: AWS::EC2::RouteTable\n Properties:\n VpcId: \n\n PublicDefaultRoute:\n Type: AWS::EC2::Route\n DependsOn: VPCGatewayAttachment\n Properties:\n RouteTableId: !Ref PublicRouteTable\n DestinationCidrBlock: 0.0.0.0/0 # CRITICAL: makes routes to the Internet\n GatewayId: !Ref InternetGateway # CRITICAL: via Internet Gateway\n\n PublicAssociation:\n Type: AWS::EC2::SubnetRouteTableAssociation\n Properties:\n SubnetId: !Ref PublicSubnet\n RouteTableId: !Ref PublicRouteTable # CRITICAL: marks this subnet as public\n\n PrivateRouteTable:\n Type: AWS::EC2::RouteTable\n Properties:\n VpcId: \n\n PrivateAssociation:\n Type: AWS::EC2::SubnetRouteTableAssociation\n Properties:\n SubnetId: !Ref PrivateSubnet\n RouteTableId: !Ref PrivateRouteTable # CRITICAL: no IGW route -> private subnet\n```", + "Other": "1. In the AWS console, go to VPC > Your VPCs and select the failing VPC\n2. Attach an Internet Gateway if none exists: Internet Gateways > Create > Attach to the VPC\n3. Create two subnets: Subnets > Create subnet\n - Subnet A (public): CIDR e.g., 10.0.1.0/24\n - Subnet B (private): CIDR e.g., 10.0.2.0/24\n4. Create a route table for public subnet: Route tables > Create\n - Add route: 0.0.0.0/0 -> Internet Gateway (IGW)\n - Associate this route table to Subnet A (public)\n5. Create a route table for private subnet: Route tables > Create\n - Do not add a route to an Internet Gateway\n - Associate this route table to Subnet B (private)\n6. Verify the VPC now has at least one subnet with an IGW route (public) and one without (private)", + "Terraform": "```hcl\n# Terraform: add one public and one private subnet to a VPC\nresource \"aws_internet_gateway\" \"\" {\n vpc_id = \"\"\n}\n\nresource \"aws_subnet\" \"public_\" {\n vpc_id = \"\"\n cidr_block = \"10.0.1.0/24\"\n}\n\nresource \"aws_subnet\" \"private_\" {\n vpc_id = \"\"\n cidr_block = \"10.0.2.0/24\"\n}\n\nresource \"aws_route_table\" \"public_\" {\n vpc_id = \"\"\n route { # CRITICAL: makes subnet public via IGW\n cidr_block = \"0.0.0.0/0\" # default route to Internet\n gateway_id = aws_internet_gateway..id\n }\n}\n\nresource \"aws_route_table_association\" \"public_assoc_\" {\n subnet_id = aws_subnet.public_.id\n route_table_id = aws_route_table.public_.id # CRITICAL\n}\n\nresource \"aws_route_table\" \"private_\" {\n vpc_id = \"\" # CRITICAL: no IGW route keeps subnet private\n}\n\nresource \"aws_route_table_association\" \"private_assoc_\" {\n subnet_id = aws_subnet.private_.id\n route_table_id = aws_route_table.private_.id # CRITICAL\n}\n```" }, "Recommendation": { - "Text": "Ensure all VPC has public and private subnets defined", - "Url": "https://docs.aws.amazon.com/vpc/latest/userguide/VPC_Scenario2.html" + "Text": "Segment each VPC: put internet-facing endpoints in **public subnets** and internal workloads in **private subnets**. Restrict ingress/egress with tight route tables, NACLs, and security groups, minimizing `0.0.0.0/0`. Apply **least privilege** and **defense in depth**. Provide controlled outbound for private subnets via managed egress and use hardened admin access patterns.", + "Url": "https://hub.prowler.com/check/vpc_subnet_separate_private_public" } }, - "Categories": [], + "Categories": [ + "trust-boundaries" + ], "DependsOn": [], "RelatedTo": [], "Notes": "" diff --git a/prowler/providers/aws/services/vpc/vpc_vpn_connection_tunnels_up/vpc_vpn_connection_tunnels_up.metadata.json b/prowler/providers/aws/services/vpc/vpc_vpn_connection_tunnels_up/vpc_vpn_connection_tunnels_up.metadata.json index f8ae7545b6..65b1c4dfa8 100644 --- a/prowler/providers/aws/services/vpc/vpc_vpn_connection_tunnels_up/vpc_vpn_connection_tunnels_up.metadata.json +++ b/prowler/providers/aws/services/vpc/vpc_vpn_connection_tunnels_up/vpc_vpn_connection_tunnels_up.metadata.json @@ -1,33 +1,38 @@ { "Provider": "aws", "CheckID": "vpc_vpn_connection_tunnels_up", - "CheckTitle": "Both VPN tunnels for an AWS Site-to-Site VPN connection should be up", + "CheckTitle": "AWS Site-to-Site VPN connection has both tunnels up", "CheckType": [ - "Software and Configuration Checks/AWS Security Best Practices" + "Software and Configuration Checks/AWS Security Best Practices/Network Reachability", + "Effects/Denial of Service" ], "ServiceName": "vpc", "SubServiceName": "", - "ResourceIdTemplate": "arn:partition:service:region:account-id:vpn-connection/resource-id", + "ResourceIdTemplate": "", "Severity": "medium", "ResourceType": "AwsEc2ClientVpnEndpoint", "ResourceGroup": "network", - "Description": "A VPN tunnel is an encrypted link where data can pass from the customer network to or from AWS within an AWS Site-to-Site VPN connection. Each VPN connection includes two VPN tunnels which you can simultaneously use for high availability. Ensuring that both VPN tunnels are up for a VPN connection is important for confirming a secure and highly available connection between an AWS VPC and your remote network.", - "Risk": "If one or both VPN tunnels are down, it can compromise the security and availability of the connection between your AWS VPC and your remote network. This could result in connectivity issues and potential data exposure or loss during the downtime, affecting business operations and overall network security.", - "RelatedUrl": "https://docs.aws.amazon.com/config/latest/developerguide/vpc-vpn-2-tunnels-up.html", + "Description": "**AWS Site-to-Site VPN** connections have two IPsec tunnels. This evaluates tunnel status and detects when any tunnel is not `UP`, indicating whether both tunnels are concurrently available for high availability.", + "Risk": "With only one active tunnel or none, the link loses redundancy, degrading **availability** and increasing the chance of outages, session drops, or route blackholing. Failover cannot occur, disrupting critical workloads and cross-environment operations.", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://docs.aws.amazon.com/vpn/latest/s2svpn/modify-vpn-tunnel-options.html", + "https://docs.aws.amazon.com/config/latest/developerguide/vpc-vpn-2-tunnels-up.html" + ], "Remediation": { "Code": { "CLI": "", "NativeIaC": "", - "Other": "https://docs.aws.amazon.com/securityhub/latest/userguide/ec2-controls.html#ec2-20", + "Other": "1. In the AWS Console, go to VPC > Site-to-Site VPN connections and select the VPN connection with a tunnel DOWN\n2. Open the Tunnel details tab\n3. For each tunnel (1 and 2), choose Actions > Download configuration, select your device/vendor, and download the config\n4. On your customer gateway device, configure BOTH tunnels using the downloaded parameters (pre-shared key, IKE/IPsec settings, inside CIDR, and routing/BGP as applicable)\n5. If your device requires different parameters, in the AWS console choose Actions > Modify VPN tunnel options, select the tunnel outside IP, adjust only the necessary options (for example IKE version or pre-shared key), and Save\n6. Wait a few minutes and verify both tunnels show Status: UP under Tunnel details", "Terraform": "" }, "Recommendation": { - "Text": "To modify VPN tunnel options, see Modifying Site-to-Site VPN tunnel options in the AWS Site-to-Site VPN User Guide.", - "Url": "https://docs.aws.amazon.com/vpn/latest/s2svpn/modify-vpn-tunnel-options.html" + "Text": "Maintain both tunnels healthy and ready for failover:\n- Deploy redundant customer gateways and resilient routing\n- Monitor tunnel health with alerts\n- Periodically test failover and document runbooks\n\nApply **high availability** and **defense-in-depth** to avoid single points of failure.", + "Url": "https://hub.prowler.com/check/vpc_vpn_connection_tunnels_up" } }, "Categories": [ - "redundancy" + "resilience" ], "DependsOn": [], "RelatedTo": [], diff --git a/prowler/providers/gcp/services/compute/compute_image_not_publicly_shared/__init__.py b/prowler/providers/gcp/services/compute/compute_image_not_publicly_shared/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/gcp/services/compute/compute_image_not_publicly_shared/compute_image_not_publicly_shared.metadata.json b/prowler/providers/gcp/services/compute/compute_image_not_publicly_shared/compute_image_not_publicly_shared.metadata.json new file mode 100644 index 0000000000..513938f322 --- /dev/null +++ b/prowler/providers/gcp/services/compute/compute_image_not_publicly_shared/compute_image_not_publicly_shared.metadata.json @@ -0,0 +1,37 @@ +{ + "Provider": "gcp", + "CheckID": "compute_image_not_publicly_shared", + "CheckTitle": "Compute Engine disk image is not publicly shared", + "CheckType": [], + "ServiceName": "compute", + "SubServiceName": "", + "ResourceIdTemplate": "", + "Severity": "medium", + "ResourceType": "compute.googleapis.com/Image", + "ResourceGroup": "compute", + "Description": "Custom disk images should not be shared publicly with **allAuthenticatedUsers**.\n\nNote: Per Google Cloud API restrictions, **allUsers** cannot be assigned to Compute Engine images. The security concern is **allAuthenticatedUsers**, which grants access to anyone with a Google account.\n\nPublicly shared disk images can expose application snapshots and sensitive data to anyone with a Google Cloud account, potentially leading to unauthorized access and data breaches.", + "Risk": "Publicly shared disk images can expose **sensitive data** and application configurations to unauthorized users.\n\n- Any authenticated GCP user can access the image content\n- Could lead to **data breaches** if images contain secrets or proprietary code\n- Attackers may use exposed images to understand application architecture", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://cloud.google.com/compute/docs/images/managing-access-custom-images", + "https://www.trendmicro.com/cloudoneconformity/knowledge-base/gcp/ComputeEngine/publicly-shared-disk-images.html" + ], + "Remediation": { + "Code": { + "CLI": "gcloud compute images remove-iam-policy-binding IMAGE_NAME --member='allAuthenticatedUsers' --role='ROLE_NAME'", + "NativeIaC": "", + "Other": "1. Go to the GCP Console\n2. Navigate to Compute Engine > Images\n3. Select the disk image\n4. Click on the INFO PANEL to view permissions\n5. Remove **allAuthenticatedUsers** bindings\n6. Click Save", + "Terraform": "```hcl\nresource \"google_compute_image_iam_binding\" \"example_resource\" {\n project = \"your-project-id\"\n image = \"your-image-name\"\n role = \"roles/compute.imageUser\"\n # Remove allAuthenticatedUsers and grant access only to specific members\n members = [\n \"user:specific-user@example.com\",\n ]\n}\n```" + }, + "Recommendation": { + "Text": "Restrict access to custom disk images by removing the **allAuthenticatedUsers** IAM binding. Apply the principle of least privilege by granting access only to specific users, groups, or service accounts that require it.", + "Url": "https://hub.prowler.com/check/compute_image_not_publicly_shared" + } + }, + "Categories": [ + "internet-exposed" + ], + "DependsOn": [], + "RelatedTo": [], + "Notes": "" +} diff --git a/prowler/providers/gcp/services/compute/compute_image_not_publicly_shared/compute_image_not_publicly_shared.py b/prowler/providers/gcp/services/compute/compute_image_not_publicly_shared/compute_image_not_publicly_shared.py new file mode 100644 index 0000000000..bd9c1875e1 --- /dev/null +++ b/prowler/providers/gcp/services/compute/compute_image_not_publicly_shared/compute_image_not_publicly_shared.py @@ -0,0 +1,39 @@ +from prowler.lib.check.models import Check, Check_Report_GCP +from prowler.providers.gcp.services.compute.compute_client import compute_client + + +class compute_image_not_publicly_shared(Check): + """Ensure Compute Engine disk images are not publicly shared. + + This check evaluates whether custom disk images in GCP Compute Engine + have IAM bindings that grant access to allAuthenticatedUsers, which allows + anyone with a Google account to access the image. + + Note: allUsers cannot be assigned to Compute Engine images (API restriction). + Only allAuthenticatedUsers can be set, which is the security risk. + Reference: https://cloud.google.com/compute/docs/images/managing-access-custom-images + + - PASS: The disk image is not publicly shared. + - FAIL: The disk image is publicly shared with allAuthenticatedUsers. + """ + + def execute(self) -> list[Check_Report_GCP]: + findings = [] + for image in compute_client.images: + report = Check_Report_GCP( + metadata=self.metadata(), + resource=image, + location="global", + ) + report.status = "PASS" + report.status_extended = ( + f"Compute Engine disk image {image.name} is not publicly shared." + ) + + if image.publicly_shared: + report.status = "FAIL" + report.status_extended = f"Compute Engine disk image {image.name} is publicly shared with allAuthenticatedUsers." + + findings.append(report) + + return findings diff --git a/prowler/providers/gcp/services/compute/compute_instance_single_network_interface/__init__.py b/prowler/providers/gcp/services/compute/compute_instance_single_network_interface/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/gcp/services/compute/compute_instance_single_network_interface/compute_instance_single_network_interface.metadata.json b/prowler/providers/gcp/services/compute/compute_instance_single_network_interface/compute_instance_single_network_interface.metadata.json new file mode 100644 index 0000000000..8c6069c6b6 --- /dev/null +++ b/prowler/providers/gcp/services/compute/compute_instance_single_network_interface/compute_instance_single_network_interface.metadata.json @@ -0,0 +1,40 @@ +{ + "Provider": "gcp", + "CheckID": "compute_instance_single_network_interface", + "CheckTitle": "VM instance has a single network interface", + "CheckType": [], + "ServiceName": "compute", + "SubServiceName": "", + "ResourceIdTemplate": "", + "Severity": "medium", + "ResourceType": "compute.googleapis.com/Instance", + "ResourceGroup": "compute", + "Description": "VM instances should be configured with only **one network interface** unless multiple interfaces are explicitly required for complex network configurations.\n\nMultiple network interfaces expand the attack surface and create additional network pathways that may be exploited.", + "Risk": "Multiple network interfaces on a VM instance can:\n\n- **Expand attack surface** by providing additional entry points for unauthorized access\n- **Create unintended network paths** that bypass security controls\n- **Increase management complexity** leading to potential misconfigurations", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://www.trendmicro.com/cloudoneconformity/knowledge-base/gcp/ComputeEngine/vms-with-multiple-enis.html", + "https://cloud.google.com/vpc/docs/multiple-interfaces-concepts" + ], + "Remediation": { + "Code": { + "CLI": "", + "NativeIaC": "", + "Other": "1. Create a machine image from the non-compliant VM instance\n2. Create a new VM instance from the machine image with only one network interface\n3. Verify the new instance is functioning correctly\n4. Delete the original multi-interface instance", + "Terraform": "```hcl\nresource \"google_compute_instance\" \"example_resource\" {\n name = \"example-instance\"\n machine_type = \"e2-medium\"\n zone = \"us-central1-a\"\n\n boot_disk {\n initialize_params {\n image = \"debian-cloud/debian-11\"\n }\n }\n\n # Only one network interface\n network_interface {\n network = \"default\"\n }\n}\n```" + }, + "Recommendation": { + "Text": "Configure VM instances with only the **minimum network connectivity** required for their intended purpose. Review instances with multiple network interfaces and consolidate to a single interface unless multi-NIC configuration is explicitly required for network appliance or routing purposes.", + "Url": "https://hub.prowler.com/check/compute_instance_single_network_interface" + } + }, + "Categories": [ + "trust-boundaries" + ], + "DependsOn": [], + "RelatedTo": [ + "compute_instance_public_ip", + "compute_instance_ip_forwarding_is_enabled" + ], + "Notes": "Instances created by GKE or used as network virtual appliances may legitimately require multiple network interfaces." +} diff --git a/prowler/providers/gcp/services/compute/compute_instance_single_network_interface/compute_instance_single_network_interface.py b/prowler/providers/gcp/services/compute/compute_instance_single_network_interface/compute_instance_single_network_interface.py new file mode 100644 index 0000000000..0fb2a1fdc5 --- /dev/null +++ b/prowler/providers/gcp/services/compute/compute_instance_single_network_interface/compute_instance_single_network_interface.py @@ -0,0 +1,43 @@ +from prowler.lib.check.models import Check, Check_Report_GCP +from prowler.providers.gcp.services.compute.compute_client import compute_client + + +class compute_instance_single_network_interface(Check): + """ + Ensure that VM instances have a single network interface. + + This check evaluates whether Compute Engine instances are configured with only + one network interface to minimize network complexity and reduce attack surface. + - PASS: The VM instance has a single network interface. + - MANUAL: The VM instance is a GKE-managed instance with multiple network interfaces + (manual review recommended as these may legitimately require multiple interfaces). + - FAIL: The VM instance has multiple network interfaces (excluding GKE instances). + """ + + def execute(self) -> list[Check_Report_GCP]: + findings = [] + for instance in compute_client.instances: + report = Check_Report_GCP(metadata=self.metadata(), resource=instance) + report.status = "PASS" + + interface_names = [nic.name for nic in instance.network_interfaces] + interface_count = len(instance.network_interfaces) + + if interface_count == 1: + report.status_extended = f"VM Instance {instance.name} has a single network interface: {interface_names[0]}." + elif interface_count > 1: + # GKE instances may legitimately require multiple network interfaces + if instance.name.startswith("gke-"): + report.status = "MANUAL" + report.status_extended = f"VM Instance {instance.name} has {interface_count} network interfaces: {', '.join(interface_names)}. This is a GKE-managed instance which may legitimately require multiple interfaces. Manual review recommended." + else: + report.status = "FAIL" + report.status_extended = f"VM Instance {instance.name} has {interface_count} network interfaces: {', '.join(interface_names)}." + else: + report.status_extended = ( + f"VM Instance {instance.name} has no network interfaces." + ) + + findings.append(report) + + return findings diff --git a/prowler/providers/gcp/services/compute/compute_service.py b/prowler/providers/gcp/services/compute/compute_service.py index 283056acb4..efe40dd765 100644 --- a/prowler/providers/gcp/services/compute/compute_service.py +++ b/prowler/providers/gcp/services/compute/compute_service.py @@ -21,6 +21,7 @@ class Compute(GCPService): self.compute_projects = [] self.load_balancers = [] self.instance_groups = [] + self.images = [] self._get_regions() self._get_projects() self._get_url_maps() @@ -34,6 +35,7 @@ class Compute(GCPService): self.__threading_call__(self._get_regional_instance_groups, self.regions) self.__threading_call__(self._get_zonal_instance_groups, self.zones) self._associate_migs_with_load_balancers() + self._get_images() def _get_regions(self): for project_id in self.project_ids: @@ -103,10 +105,30 @@ class Compute(GCPService): for instance in response.get("items", []): public_ip = False - for interface in instance.get("networkInterfaces", []): + network_interfaces_raw = instance.get("networkInterfaces", []) + + network_interfaces = [] + for interface in network_interfaces_raw: for config in interface.get("accessConfigs", []): if "natIP" in config: public_ip = True + + network_interfaces.append( + NetworkInterface( + name=interface.get("name", ""), + network=( + interface.get("network", "").split("/")[-1] + if interface.get("network") + else "" + ), + subnetwork=( + interface.get("subnetwork", "").split("/")[-1] + if interface.get("subnetwork") + else "" + ), + ) + ) + self.instances.append( Instance( name=instance["name"], @@ -165,6 +187,7 @@ class Compute(GCPService): deletion_protection=instance.get( "deletionProtection", False ), + network_interfaces=network_interfaces, ) ) @@ -533,6 +556,58 @@ class Compute(GCPService): if (mig.project_id, mig.name) in load_balanced_groups: mig.load_balanced = True + def _get_images(self) -> None: + for project_id in self.project_ids: + try: + request = self.client.images().list(project=project_id) + while request is not None: + response = request.execute(num_retries=DEFAULT_RETRY_ATTEMPTS) + for image in response.get("items", []): + publicly_shared = False + try: + iam_policy = ( + self.client.images() + .getIamPolicy( + project=project_id, resource=image["name"] + ) + .execute(num_retries=DEFAULT_RETRY_ATTEMPTS) + ) + for binding in iam_policy.get("bindings", []): + # allUsers cannot be assigned to Compute Engine images (API restriction). + # Only allAuthenticatedUsers can be set, which is the security risk. + if "allAuthenticatedUsers" in binding.get( + "members", [] + ): + publicly_shared = True + break + except Exception as error: + logger.error( + f"{project_id}/{image['name']} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" + ) + + self.images.append( + Image( + name=image["name"], + id=image["id"], + project_id=project_id, + publicly_shared=publicly_shared, + ) + ) + + request = self.client.images().list_next( + previous_request=request, previous_response=response + ) + except Exception as error: + logger.error( + f"{project_id} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" + ) + + +class NetworkInterface(BaseModel): + name: str + network: str = "" + subnetwork: str = "" + class Disk(BaseModel): name: str @@ -560,6 +635,7 @@ class Instance(BaseModel): preemptible: bool = False provisioning_model: str = "STANDARD" deletion_protection: bool = False + network_interfaces: list[NetworkInterface] = [] class Network(BaseModel): @@ -625,3 +701,10 @@ class ManagedInstanceGroup(BaseModel): project_id: str auto_healing_policies: list[AutoHealingPolicy] = [] load_balanced: bool = False + + +class Image(BaseModel): + name: str + id: str + project_id: str + publicly_shared: bool = False diff --git a/prowler/providers/gcp/services/logging/logging_log_metric_filter_and_alert_for_compute_configuration_changes_enabled/__init__.py b/prowler/providers/gcp/services/logging/logging_log_metric_filter_and_alert_for_compute_configuration_changes_enabled/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/gcp/services/logging/logging_log_metric_filter_and_alert_for_compute_configuration_changes_enabled/logging_log_metric_filter_and_alert_for_compute_configuration_changes_enabled.metadata.json b/prowler/providers/gcp/services/logging/logging_log_metric_filter_and_alert_for_compute_configuration_changes_enabled/logging_log_metric_filter_and_alert_for_compute_configuration_changes_enabled.metadata.json new file mode 100644 index 0000000000..9cdbbc51e8 --- /dev/null +++ b/prowler/providers/gcp/services/logging/logging_log_metric_filter_and_alert_for_compute_configuration_changes_enabled/logging_log_metric_filter_and_alert_for_compute_configuration_changes_enabled.metadata.json @@ -0,0 +1,38 @@ +{ + "Provider": "gcp", + "CheckID": "logging_log_metric_filter_and_alert_for_compute_configuration_changes_enabled", + "CheckTitle": "Compute Engine configuration changes are monitored with log metric filters and alerts", + "CheckType": [], + "ServiceName": "logging", + "SubServiceName": "", + "ResourceIdTemplate": "", + "Severity": "medium", + "ResourceType": "MetricFilter", + "ResourceGroup": "monitoring", + "Description": "Log metric filters and alerts for **Compute Engine configuration changes** provide visibility into modifications to instances, disks, networks, firewalls, and routes. These monitoring controls enable security teams to detect unauthorized changes and investigate suspicious infrastructure modifications.", + "Risk": "Without monitoring for Compute Engine configuration changes, **unauthorized modifications** to compute resources may go undetected. Attackers can establish **persistence** through instance modifications, escalate privileges via IAM policy changes, disable security controls, or pivot to other resources. This compromises **confidentiality**, **integrity**, and **availability** of workloads and may enable **data exfiltration** or **lateral movement**.", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://www.trendmicro.com/cloudoneconformity/knowledge-base/gcp/ComputeEngine/gcp-compute-engine-configuration-changes.html", + "https://cloud.google.com/logging/docs/audit", + "https://cloud.google.com/monitoring/alerts" + ], + "Remediation": { + "Code": { + "CLI": "gcloud logging metrics create compute_config_changes --description=\"Compute Engine configuration changes\" --log-filter='protoPayload.serviceName=\"compute.googleapis.com\"' && gcloud alpha monitoring policies create --notification-channels=CHANNEL_ID --display-name=\"Compute Engine Configuration Changes Alert\" --condition-threshold-value=1 --condition-threshold-duration=0s --condition-filter='metric.type=\"logging.googleapis.com/user/compute_config_changes\"'", + "NativeIaC": "", + "Other": "1. Open the Google Cloud Console\n2. Navigate to Logging > Logs-based Metrics\n3. Click 'Create Metric'\n4. Set Metric Type to 'Counter'\n5. Enter filter: protoPayload.serviceName=\"compute.googleapis.com\"\n6. Click 'Create Metric'\n7. Navigate to Monitoring > Alerting\n8. Click 'Create Policy'\n9. Click 'Add Condition'\n10. Select your log metric in the metric dropdown\n11. Set threshold and conditions\n12. Add notification channels\n13. Click 'Save'", + "Terraform": "```hcl\nresource \"google_logging_metric\" \"compute_config_changes\" {\n name = \"compute_config_changes\"\n filter = \"protoPayload.serviceName=\\\"compute.googleapis.com\\\"\"\n metric_descriptor {\n metric_kind = \"DELTA\"\n value_type = \"INT64\"\n }\n}\n\nresource \"google_monitoring_alert_policy\" \"compute_config_alert\" {\n display_name = \"Compute Engine Configuration Changes\"\n conditions {\n display_name = \"Compute config changes detected\"\n condition_threshold {\n filter = \"metric.type=\\\"logging.googleapis.com/user/compute_config_changes\\\"\"\n duration = \"0s\"\n comparison = \"COMPARISON_GT\"\n threshold_value = 0\n }\n }\n notification_channels = [var.notification_channel_id]\n}\n```" + }, + "Recommendation": { + "Text": "Configure log-based metric filters to detect Compute Engine configuration changes and create alert policies that trigger notifications when these metrics increment. Apply the **principle of least privilege** to limit who can modify compute resources, and establish **change management processes** to review and approve infrastructure modifications.", + "Url": "https://hub.prowler.com/check/logging_log_metric_filter_and_alert_for_compute_configuration_changes_enabled" + } + }, + "Categories": [ + "logging" + ], + "DependsOn": [], + "RelatedTo": [], + "Notes": "" +} diff --git a/prowler/providers/gcp/services/logging/logging_log_metric_filter_and_alert_for_compute_configuration_changes_enabled/logging_log_metric_filter_and_alert_for_compute_configuration_changes_enabled.py b/prowler/providers/gcp/services/logging/logging_log_metric_filter_and_alert_for_compute_configuration_changes_enabled/logging_log_metric_filter_and_alert_for_compute_configuration_changes_enabled.py new file mode 100644 index 0000000000..7902f9ed72 --- /dev/null +++ b/prowler/providers/gcp/services/logging/logging_log_metric_filter_and_alert_for_compute_configuration_changes_enabled/logging_log_metric_filter_and_alert_for_compute_configuration_changes_enabled.py @@ -0,0 +1,50 @@ +from prowler.lib.check.models import Check, Check_Report_GCP +from prowler.providers.gcp.services.logging.logging_client import logging_client +from prowler.providers.gcp.services.monitoring.monitoring_client import ( + monitoring_client, +) + + +class logging_log_metric_filter_and_alert_for_compute_configuration_changes_enabled( + Check +): + def execute(self) -> Check_Report_GCP: + findings = [] + projects_with_metric = set() + for metric in logging_client.metrics: + if 'protoPayload.serviceName="compute.googleapis.com"' in metric.filter: + report = Check_Report_GCP( + metadata=self.metadata(), + resource=metric, + location=logging_client.region, + resource_name=metric.name if metric.name else "Log Metric Filter", + ) + projects_with_metric.add(metric.project_id) + report.status = "FAIL" + report.status_extended = f"Log metric filter {metric.name} found but no alerts associated in project {metric.project_id}." + for alert_policy in monitoring_client.alert_policies: + for filter in alert_policy.filters: + if metric.name in filter: + report.status = "PASS" + report.status_extended = f"Log metric filter {metric.name} found with alert policy {alert_policy.display_name} associated in project {metric.project_id}." + break + findings.append(report) + + for project in logging_client.project_ids: + if project not in projects_with_metric: + report = Check_Report_GCP( + metadata=self.metadata(), + resource=logging_client.projects[project], + project_id=project, + location=logging_client.region, + resource_name=( + logging_client.projects[project].name + if logging_client.projects[project].name + else "GCP Project" + ), + ) + report.status = "FAIL" + report.status_extended = f"There are no log metric filters or alerts associated for Compute Engine configuration changes in project {project}." + findings.append(report) + + return findings diff --git a/prowler/providers/oraclecloud/oraclecloud_provider.py b/prowler/providers/oraclecloud/oraclecloud_provider.py index 08d654d580..3ff83e7c22 100644 --- a/prowler/providers/oraclecloud/oraclecloud_provider.py +++ b/prowler/providers/oraclecloud/oraclecloud_provider.py @@ -266,7 +266,6 @@ class OraclecloudProvider(Provider): # If API key credentials are provided directly, create config from them if user and fingerprint and tenancy and region: import base64 - import tempfile logger.info("Using API key credentials from direct parameters") @@ -280,21 +279,19 @@ class OraclecloudProvider(Provider): # Handle private key if key_content: - # Decode base64 key content and write to temp file + # Decode base64 key content try: key_data = base64.b64decode(key_content) - temp_key_file = tempfile.NamedTemporaryFile( - mode="wb", delete=False, suffix=".pem" - ) - temp_key_file.write(key_data) - temp_key_file.close() - config["key_file"] = temp_key_file.name + decoded_key = key_data.decode("utf-8") except Exception as decode_error: logger.error(f"Failed to decode key_content: {decode_error}") raise OCIInvalidConfigError( file=pathlib.Path(__file__).name, message="Failed to decode key_content. Ensure it is base64 encoded.", ) + + # Use OCI SDK's native key_content support + config["key_content"] = decoded_key elif key_file: config["key_file"] = os.path.expanduser(key_file) else: @@ -428,78 +425,85 @@ class OraclecloudProvider(Provider): Raises: - OCIAuthenticationError: If authentication fails. """ - try: - # Get tenancy from config - tenancy_id = session.config.get("tenancy") + # Get tenancy from config + tenancy_id = session.config.get("tenancy") - if not tenancy_id: - raise OCINoCredentialsError( - file=pathlib.Path(__file__).name, - message="Tenancy ID not found in configuration", - ) - - # Validate tenancy OCID format - if not OraclecloudProvider.validate_ocid(tenancy_id, "tenancy"): - raise OCIInvalidTenancyError( - file=pathlib.Path(__file__).name, - message=f"Invalid tenancy OCID format: {tenancy_id}", - ) - - # Get user from config (not available in instance principal) - user_id = session.config.get("user", "instance-principal") - - # Get region from config or use provided region - if not region: - region = session.config.get("region", "us-ashburn-1") - - # Validate region - if region not in OCI_REGIONS: - raise OCIInvalidRegionError( - file=pathlib.Path(__file__).name, - message=f"Invalid region: {region}", - ) - - # Get tenancy name using Identity service - tenancy_name = "unknown" - try: - # Create identity client with proper authentication handling - if session.signer: - identity_client = oci.identity.IdentityClient( - config=session.config, signer=session.signer - ) - else: - identity_client = oci.identity.IdentityClient(config=session.config) - - tenancy = identity_client.get_tenancy(tenancy_id).data - tenancy_name = tenancy.name - logger.info(f"Tenancy Name: {tenancy_name}") - except Exception as error: - logger.warning( - f"Could not retrieve tenancy name: {error}. Using 'unknown'" - ) - - logger.info(f"OCI Tenancy ID: {tenancy_id}") - logger.info(f"OCI User ID: {user_id}") - logger.info(f"OCI Region: {region}") - - return OCIIdentityInfo( - tenancy_id=tenancy_id, - tenancy_name=tenancy_name, - user_id=user_id, - region=region, - profile=session.profile, - audited_regions=set([region]) if region else set(), - audited_compartments=compartment_ids if compartment_ids else [], + if not tenancy_id: + raise OCINoCredentialsError( + file=pathlib.Path(__file__).name, + message="Tenancy ID not found in configuration", ) - except Exception as error: + # Validate tenancy OCID format + if not OraclecloudProvider.validate_ocid(tenancy_id, "tenancy"): + raise OCIInvalidTenancyError( + file=pathlib.Path(__file__).name, + message=f"Invalid tenancy OCID format: {tenancy_id}", + ) + + # Get user from config (not available in instance principal) + user_id = session.config.get("user", "instance-principal") + + # Get region from config or use provided region + if not region: + region = session.config.get("region", "us-ashburn-1") + + # Validate region + if region not in OCI_REGIONS: + raise OCIInvalidRegionError( + file=pathlib.Path(__file__).name, + message=f"Invalid region: {region}", + ) + + # Validate credentials by calling OCI Identity service + try: + if session.signer: + identity_client = oci.identity.IdentityClient( + config=session.config, signer=session.signer + ) + else: + identity_client = oci.identity.IdentityClient(config=session.config) + + tenancy = identity_client.get_tenancy(tenancy_id).data + tenancy_name = tenancy.name + logger.info(f"Tenancy Name: {tenancy_name}") + except oci.exceptions.ServiceError as error: logger.critical( - f"OCIAuthenticationError[{error.__traceback__.tb_lineno}]: {error}" + f"OCI credential validation failed (HTTP {error.status}): {error.message}" ) raise OCIAuthenticationError( - original_exception=error, file=pathlib.Path(__file__).name, + message=f"OCI credential validation failed: {error.message}. Please verify your credentials and try again.", + original_exception=error, ) + except oci.exceptions.InvalidPrivateKey as error: + logger.critical(f"Invalid OCI private key: {error}") + raise OCIAuthenticationError( + file=pathlib.Path(__file__).name, + message="Invalid OCI private key format. Ensure the key is a valid PEM-encoded private key.", + original_exception=error, + ) + except Exception as error: + logger.critical(f"OCI authentication error: {error}") + raise OCIAuthenticationError( + file=pathlib.Path(__file__).name, + message=f"Failed to authenticate with OCI: {error}", + original_exception=error, + ) + + logger.info(f"OCI Tenancy ID: {tenancy_id}") + logger.info(f"OCI User ID: {user_id}") + logger.info(f"OCI Region: {region}") + + return OCIIdentityInfo( + tenancy_id=tenancy_id, + tenancy_name=tenancy_name, + user_id=user_id, + region=region, + profile=session.profile, + audited_regions=set([region]) if region else set(), + audited_compartments=compartment_ids if compartment_ids else [], + ) @staticmethod def validate_ocid(ocid: str, resource_type: str = None) -> bool: @@ -838,7 +842,6 @@ class OraclecloudProvider(Provider): # If API key credentials are provided directly, create config from them if user and fingerprint and tenancy and region: import base64 - import tempfile logger.info("Using API key credentials from direct parameters") @@ -852,21 +855,19 @@ class OraclecloudProvider(Provider): # Handle private key if key_content: - # Decode base64 key content and write to temp file + # Decode base64 key content try: key_data = base64.b64decode(key_content) - temp_key_file = tempfile.NamedTemporaryFile( - mode="wb", delete=False, suffix=".pem" - ) - temp_key_file.write(key_data) - temp_key_file.close() - config["key_file"] = temp_key_file.name + decoded_key = key_data.decode("utf-8") except Exception as decode_error: logger.error(f"Failed to decode key_content: {decode_error}") raise OCIInvalidConfigError( file=pathlib.Path(__file__).name, message="Failed to decode key_content. Ensure it is base64 encoded.", ) + + # Use OCI SDK's native key_content support + config["key_content"] = decoded_key elif key_file: config["key_file"] = os.path.expanduser(key_file) else: diff --git a/skills/README.md b/skills/README.md new file mode 100644 index 0000000000..becdffaf9b --- /dev/null +++ b/skills/README.md @@ -0,0 +1,127 @@ +# AI Agent Skills + +This directory contains **Agent Skills** following the [Agent Skills open standard](https://agentskills.io). Skills provide domain-specific patterns, conventions, and guardrails that help AI coding assistants (Claude Code, OpenCode, Cursor, etc.) understand project-specific requirements. + +## What Are Skills? + +[Agent Skills](https://agentskills.io) is an open standard format for extending AI agent capabilities with specialized knowledge. Originally developed by Anthropic and released as an open standard, it is now adopted by multiple agent products. + +Skills teach AI assistants how to perform specific tasks. When an AI loads a skill, it gains context about: + +- Critical rules (what to always/never do) +- Code patterns and conventions +- Project-specific workflows +- References to detailed documentation + +## Setup + +Run the setup script to configure skills for all supported AI coding assistants: + +```bash +./skills/setup.sh +``` + +This creates symlinks so each tool finds skills in its expected location: + +| Tool | Symlink Created | +|------|-----------------| +| Claude Code / OpenCode | `.claude/skills/` | +| Codex (OpenAI) | `.codex/skills/` | +| GitHub Copilot | `.github/skills/` | +| Gemini CLI | `.gemini/skills/` | + +After running setup, restart your AI coding assistant to load the skills. + +## How to Use Skills + +Skills are automatically discovered by the AI agent. To manually load a skill during a session: + +``` +Read skills/{skill-name}/SKILL.md +``` + +## Available Skills + +### Generic Skills + +Reusable patterns for common technologies: + +| Skill | Description | +|-------|-------------| +| `typescript` | Const types, flat interfaces, utility types | +| `react-19` | React 19 patterns, React Compiler | +| `nextjs-15` | App Router, Server Actions, streaming | +| `tailwind-4` | cn() utility, Tailwind 4 patterns | +| `playwright` | Page Object Model, selectors | +| `pytest` | Fixtures, mocking, markers | +| `django-drf` | ViewSets, Serializers, Filters | +| `zod-4` | Zod 4 API patterns | +| `zustand-5` | Persist, selectors, slices | +| `ai-sdk-5` | Vercel AI SDK patterns | + +### Prowler-Specific Skills + +Patterns tailored for Prowler development: + +| Skill | Description | +|-------|-------------| +| `prowler` | Project overview, component navigation | +| `prowler-api` | Django + RLS + JSON:API patterns | +| `prowler-ui` | Next.js + shadcn conventions | +| `prowler-sdk-check` | Create new security checks | +| `prowler-mcp` | MCP server tools and models | +| `prowler-test-sdk` | SDK testing (pytest + moto) | +| `prowler-test-api` | API testing (pytest-django + RLS) | +| `prowler-test-ui` | E2E testing (Playwright) | +| `prowler-compliance` | Compliance framework structure | +| `prowler-provider` | Add new cloud providers | +| `prowler-pr` | Pull request conventions | +| `prowler-docs` | Documentation style guide | + +### Meta Skills + +| Skill | Description | +|-------|-------------| +| `skill-creator` | Create new AI agent skills | + +## Directory Structure + +``` +skills/ +├── {skill-name}/ +│ ├── SKILL.md # Required - main instrunsction and metadata +│ ├── scripts/ # Optional - executable code +│ ├── assets/ # Optional - templates, schemas, resources +│ └── references/ # Optional - links to local docs +└── README.md # This file +``` + +## Creating New Skills + +Use the `skill-creator` skill for guidance: + +``` +Read skills/skill-creator/SKILL.md +``` + +### Quick Checklist + +1. Create directory: `skills/{skill-name}/` +2. Add `SKILL.md` with required frontmatter +3. Keep content concise (under 500 lines) +4. Reference existing docs instead of duplicating +5. Add to `AGENTS.md` skills table + +## Design Principles + +- **Concise**: Only include what AI doesn't already know +- **Progressive disclosure**: Point to detailed docs, don't duplicate +- **Critical rules first**: Lead with ALWAYS/NEVER patterns +- **Minimal examples**: Show patterns, not tutorials + +## Resources + +- [Agent Skills Standard](https://agentskills.io) - Open standard specification +- [Agent Skills GitHub](https://github.com/anthropics/skills) - Example skills +- [Claude Code Best Practices](https://platform.claude.com/docs/en/agents-and-tools/agent-skills/best-practices) - Skill authoring guide +- [Prowler AGENTS.md](../AGENTS.md) - AI agent general rules diff --git a/skills/ai-sdk-5/SKILL.md b/skills/ai-sdk-5/SKILL.md new file mode 100644 index 0000000000..3b1c979f73 --- /dev/null +++ b/skills/ai-sdk-5/SKILL.md @@ -0,0 +1,234 @@ +--- +name: ai-sdk-5 +description: > + Vercel AI SDK 5 patterns. + Trigger: When building AI chat features - breaking changes from v4. +license: Apache-2.0 +metadata: + author: prowler-cloud + version: "1.0" +allowed-tools: Read, Edit, Write, Glob, Grep, Bash, WebFetch, WebSearch, Task +--- + +## Breaking Changes from AI SDK 4 + +```typescript +// ❌ AI SDK 4 (OLD) +import { useChat } from "ai"; +const { messages, handleSubmit, input, handleInputChange } = useChat({ + api: "/api/chat", +}); + +// ✅ AI SDK 5 (NEW) +import { useChat } from "@ai-sdk/react"; +import { DefaultChatTransport } from "ai"; + +const { messages, sendMessage } = useChat({ + transport: new DefaultChatTransport({ api: "/api/chat" }), +}); +``` + +## Client Setup + +```typescript +import { useChat } from "@ai-sdk/react"; +import { DefaultChatTransport } from "ai"; +import { useState } from "react"; + +export function Chat() { + const [input, setInput] = useState(""); + + const { messages, sendMessage, isLoading, error } = useChat({ + transport: new DefaultChatTransport({ api: "/api/chat" }), + }); + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault(); + if (!input.trim()) return; + sendMessage({ text: input }); + setInput(""); + }; + + return ( +
+
+ {messages.map((message) => ( + + ))} +
+ +
+ setInput(e.target.value)} + placeholder="Type a message..." + disabled={isLoading} + /> + +
+ + {error &&
Error: {error.message}
} +
+ ); +} +``` + +## UIMessage Structure (v5) + +```typescript +// ❌ Old: message.content was a string +// ✅ New: message.parts is an array + +interface UIMessage { + id: string; + role: "user" | "assistant" | "system"; + parts: MessagePart[]; +} + +type MessagePart = + | { type: "text"; text: string } + | { type: "image"; image: string } + | { type: "tool-call"; toolCallId: string; toolName: string; args: unknown } + | { type: "tool-result"; toolCallId: string; result: unknown }; + +// Extract text from parts +function getMessageText(message: UIMessage): string { + return message.parts + .filter((part): part is { type: "text"; text: string } => part.type === "text") + .map((part) => part.text) + .join(""); +} + +// Render message +function Message({ message }: { message: UIMessage }) { + return ( +
+ {message.parts.map((part, index) => { + if (part.type === "text") { + return

{part.text}

; + } + if (part.type === "image") { + return ; + } + return null; + })} +
+ ); +} +``` + +## Server-Side (Route Handler) + +```typescript +// app/api/chat/route.ts +import { openai } from "@ai-sdk/openai"; +import { streamText } from "ai"; + +export async function POST(req: Request) { + const { messages } = await req.json(); + + const result = await streamText({ + model: openai("gpt-4o"), + messages, + system: "You are a helpful assistant.", + }); + + return result.toDataStreamResponse(); +} +``` + +## With LangChain + +```typescript +// app/api/chat/route.ts +import { toUIMessageStream } from "@ai-sdk/langchain"; +import { ChatOpenAI } from "@langchain/openai"; +import { HumanMessage, AIMessage } from "@langchain/core/messages"; + +export async function POST(req: Request) { + const { messages } = await req.json(); + + const model = new ChatOpenAI({ + modelName: "gpt-4o", + streaming: true, + }); + + // Convert UI messages to LangChain format + const langchainMessages = messages.map((m) => { + const text = m.parts + .filter((p) => p.type === "text") + .map((p) => p.text) + .join(""); + return m.role === "user" + ? new HumanMessage(text) + : new AIMessage(text); + }); + + const stream = await model.stream(langchainMessages); + + return toUIMessageStream(stream).toDataStreamResponse(); +} +``` + +## Streaming with Tools + +```typescript +import { openai } from "@ai-sdk/openai"; +import { streamText, tool } from "ai"; +import { z } from "zod"; + +const result = await streamText({ + model: openai("gpt-4o"), + messages, + tools: { + getWeather: tool({ + description: "Get weather for a location", + parameters: z.object({ + location: z.string().describe("City name"), + }), + execute: async ({ location }) => { + // Fetch weather data + return { temperature: 72, condition: "sunny" }; + }, + }), + }, +}); +``` + +## useCompletion (Text Generation) + +```typescript +import { useCompletion } from "@ai-sdk/react"; +import { DefaultCompletionTransport } from "ai"; + +const { completion, complete, isLoading } = useCompletion({ + transport: new DefaultCompletionTransport({ api: "/api/complete" }), +}); + +// Trigger completion +await complete("Write a haiku about"); +``` + +## Error Handling + +```typescript +const { error, messages, sendMessage } = useChat({ + transport: new DefaultChatTransport({ api: "/api/chat" }), + onError: (error) => { + console.error("Chat error:", error); + toast.error("Failed to send message"); + }, +}); + +// Display error +{error && ( +
+ {error.message} + +
+)} +``` diff --git a/skills/django-drf/SKILL.md b/skills/django-drf/SKILL.md new file mode 100644 index 0000000000..03b43fc64e --- /dev/null +++ b/skills/django-drf/SKILL.md @@ -0,0 +1,184 @@ +--- +name: django-drf +description: > + Django REST Framework patterns. + Trigger: When building REST APIs with Django - ViewSets, Serializers, Filters. +license: Apache-2.0 +metadata: + author: prowler-cloud + version: "1.0" +allowed-tools: Read, Edit, Write, Glob, Grep, Bash, WebFetch, WebSearch, Task +--- + +## ViewSet Pattern + +```python +from rest_framework import viewsets, status +from rest_framework.response import Response +from rest_framework.decorators import action + +class UserViewSet(viewsets.ModelViewSet): + queryset = User.objects.all() + serializer_class = UserSerializer + filterset_class = UserFilter + permission_classes = [IsAuthenticated] + + def get_serializer_class(self): + if self.action == "create": + return UserCreateSerializer + if self.action in ["update", "partial_update"]: + return UserUpdateSerializer + return UserSerializer + + @action(detail=True, methods=["post"]) + def activate(self, request, pk=None): + user = self.get_object() + user.is_active = True + user.save() + return Response({"status": "activated"}) +``` + +## Serializer Patterns + +```python +from rest_framework import serializers + +# Read Serializer +class UserSerializer(serializers.ModelSerializer): + full_name = serializers.SerializerMethodField() + + class Meta: + model = User + fields = ["id", "email", "full_name", "created_at"] + read_only_fields = ["id", "created_at"] + + def get_full_name(self, obj): + return f"{obj.first_name} {obj.last_name}" + +# Create Serializer +class UserCreateSerializer(serializers.ModelSerializer): + password = serializers.CharField(write_only=True) + + class Meta: + model = User + fields = ["email", "password", "first_name", "last_name"] + + def create(self, validated_data): + password = validated_data.pop("password") + user = User(**validated_data) + user.set_password(password) + user.save() + return user + +# Update Serializer +class UserUpdateSerializer(serializers.ModelSerializer): + class Meta: + model = User + fields = ["first_name", "last_name"] +``` + +## Filters + +```python +from django_filters import rest_framework as filters + +class UserFilter(filters.FilterSet): + email = filters.CharFilter(lookup_expr="icontains") + is_active = filters.BooleanFilter() + created_after = filters.DateTimeFilter( + field_name="created_at", + lookup_expr="gte" + ) + created_before = filters.DateTimeFilter( + field_name="created_at", + lookup_expr="lte" + ) + + class Meta: + model = User + fields = ["email", "is_active"] +``` + +## Permissions + +```python +from rest_framework.permissions import BasePermission + +class IsOwner(BasePermission): + def has_object_permission(self, request, view, obj): + return obj.owner == request.user + +class IsAdminOrReadOnly(BasePermission): + def has_permission(self, request, view): + if request.method in ["GET", "HEAD", "OPTIONS"]: + return True + return request.user.is_staff +``` + +## Pagination + +```python +from rest_framework.pagination import PageNumberPagination + +class StandardPagination(PageNumberPagination): + page_size = 20 + page_size_query_param = "page_size" + max_page_size = 100 + +# settings.py +REST_FRAMEWORK = { + "DEFAULT_PAGINATION_CLASS": "api.pagination.StandardPagination", +} +``` + +## URL Routing + +```python +from rest_framework.routers import DefaultRouter + +router = DefaultRouter() +router.register(r"users", UserViewSet, basename="user") +router.register(r"posts", PostViewSet, basename="post") + +urlpatterns = [ + path("api/v1/", include(router.urls)), +] +``` + +## Testing + +```python +import pytest +from rest_framework import status +from rest_framework.test import APIClient + +@pytest.fixture +def api_client(): + return APIClient() + +@pytest.fixture +def authenticated_client(api_client, user): + api_client.force_authenticate(user=user) + return api_client + +@pytest.mark.django_db +class TestUserViewSet: + def test_list_users(self, authenticated_client): + response = authenticated_client.get("/api/v1/users/") + assert response.status_code == status.HTTP_200_OK + + def test_create_user(self, authenticated_client): + data = {"email": "new@test.com", "password": "pass123"} + response = authenticated_client.post("/api/v1/users/", data) + assert response.status_code == status.HTTP_201_CREATED +``` + +## Commands + +```bash +python manage.py runserver +python manage.py makemigrations +python manage.py migrate +python manage.py createsuperuser +python manage.py shell +``` diff --git a/skills/nextjs-15/SKILL.md b/skills/nextjs-15/SKILL.md new file mode 100644 index 0000000000..4fa47fd619 --- /dev/null +++ b/skills/nextjs-15/SKILL.md @@ -0,0 +1,148 @@ +--- +name: nextjs-15 +description: > + Next.js 15 App Router patterns. + Trigger: When working with Next.js - routing, Server Actions, data fetching. +license: Apache-2.0 +metadata: + author: prowler-cloud + version: "1.0" +allowed-tools: Read, Edit, Write, Glob, Grep, Bash, WebFetch, WebSearch, Task +--- + +## App Router File Conventions + +``` +app/ +├── layout.tsx # Root layout (required) +├── page.tsx # Home page (/) +├── loading.tsx # Loading UI (Suspense) +├── error.tsx # Error boundary +├── not-found.tsx # 404 page +├── (auth)/ # Route group (no URL impact) +│ ├── login/page.tsx # /login +│ └── signup/page.tsx # /signup +├── api/ +│ └── route.ts # API handler +└── _components/ # Private folder (not routed) +``` + +## Server Components (Default) + +```typescript +// No directive needed - async by default +export default async function Page() { + const data = await db.query(); + return ; +} +``` + +## Server Actions + +```typescript +// app/actions.ts +"use server"; + +import { revalidatePath } from "next/cache"; +import { redirect } from "next/navigation"; + +export async function createUser(formData: FormData) { + const name = formData.get("name") as string; + + await db.users.create({ data: { name } }); + + revalidatePath("/users"); + redirect("/users"); +} + +// Usage +
+ + +
+``` + +## Data Fetching + +```typescript +// Parallel +async function Page() { + const [users, posts] = await Promise.all([ + getUsers(), + getPosts(), + ]); + return ; +} + +// Streaming with Suspense +}> + + +``` + +## Route Handlers (API) + +```typescript +// app/api/users/route.ts +import { NextRequest, NextResponse } from "next/server"; + +export async function GET(request: NextRequest) { + const users = await db.users.findMany(); + return NextResponse.json(users); +} + +export async function POST(request: NextRequest) { + const body = await request.json(); + const user = await db.users.create({ data: body }); + return NextResponse.json(user, { status: 201 }); +} +``` + +## Middleware + +```typescript +// middleware.ts (root level) +import { NextResponse } from "next/server"; +import type { NextRequest } from "next/server"; + +export function middleware(request: NextRequest) { + const token = request.cookies.get("token"); + + if (!token && request.nextUrl.pathname.startsWith("/dashboard")) { + return NextResponse.redirect(new URL("/login", request.url)); + } + + return NextResponse.next(); +} + +export const config = { + matcher: ["/dashboard/:path*"], +}; +``` + +## Metadata + +```typescript +// Static +export const metadata = { + title: "My App", + description: "Description", +}; + +// Dynamic +export async function generateMetadata({ params }) { + const product = await getProduct(params.id); + return { title: product.name }; +} +``` + +## server-only Package + +```typescript +import "server-only"; + +// This will error if imported in client component +export async function getSecretData() { + return db.secrets.findMany(); +} +``` diff --git a/skills/playwright/SKILL.md b/skills/playwright/SKILL.md new file mode 100644 index 0000000000..34e71aadd1 --- /dev/null +++ b/skills/playwright/SKILL.md @@ -0,0 +1,324 @@ +--- +name: playwright +description: > + Playwright E2E testing patterns. + Trigger: When writing E2E tests - Page Objects, selectors, MCP workflow. +license: Apache-2.0 +metadata: + author: prowler-cloud + version: "1.0" +allowed-tools: Read, Edit, Write, Glob, Grep, Bash, WebFetch, WebSearch, Task +--- + +## MCP Workflow (MANDATORY If Available) + +**⚠️ If you have Playwright MCP tools, ALWAYS use them BEFORE creating any test:** + +1. **Navigate** to target page +2. **Take snapshot** to see page structure and elements +3. **Interact** with forms/elements to verify exact user flow +4. **Take screenshots** to document expected states +5. **Verify page transitions** through complete flow (loading, success, error) +6. **Document actual selectors** from snapshots (use real refs and labels) +7. **Only after exploring** create test code with verified selectors + +**If MCP NOT available:** Proceed with test creation based on docs and code analysis. + +**Why This Matters:** +- ✅ Precise tests - exact steps needed, no assumptions +- ✅ Accurate selectors - real DOM structure, not imagined +- ✅ Real flow validation - verify journey actually works +- ✅ Avoid over-engineering - minimal tests for what exists +- ✅ Prevent flaky tests - real exploration = stable tests +- ❌ Never assume how UI "should" work + +## File Structure + +``` +tests/ +├── base-page.ts # Parent class for ALL pages +├── helpers.ts # Shared utilities +└── {page-name}/ + ├── {page-name}-page.ts # Page Object Model + ├── {page-name}.spec.ts # ALL tests here (NO separate files!) + └── {page-name}.md # Test documentation +``` + +**File Naming:** +- ✅ `sign-up.spec.ts` (all sign-up tests) +- ✅ `sign-up-page.ts` (page object) +- ✅ `sign-up.md` (documentation) +- ❌ `sign-up-critical-path.spec.ts` (WRONG - no separate files) +- ❌ `sign-up-validation.spec.ts` (WRONG) + +## Selector Priority (REQUIRED) + +```typescript +// 1. BEST - getByRole for interactive elements +this.submitButton = page.getByRole("button", { name: "Submit" }); +this.navLink = page.getByRole("link", { name: "Dashboard" }); + +// 2. BEST - getByLabel for form controls +this.emailInput = page.getByLabel("Email"); +this.passwordInput = page.getByLabel("Password"); + +// 3. SPARINGLY - getByText for static content only +this.errorMessage = page.getByText("Invalid credentials"); +this.pageTitle = page.getByText("Welcome"); + +// 4. LAST RESORT - getByTestId when above fail +this.customWidget = page.getByTestId("date-picker"); + +// ❌ AVOID fragile selectors +this.button = page.locator(".btn-primary"); // NO +this.input = page.locator("#email"); // NO +``` + +## Scope Detection (ASK IF AMBIGUOUS) + +| User Says | Action | +|-----------|--------| +| "a test", "one test", "new test", "add test" | Create ONE test() in existing spec | +| "comprehensive tests", "all tests", "test suite", "generate tests" | Create full suite | + +**Examples:** +- "Create a test for user sign-up" → ONE test only +- "Generate E2E tests for login page" → Full suite +- "Add a test to verify form validation" → ONE test to existing spec + +## Page Object Pattern + +```typescript +import { Page, Locator, expect } from "@playwright/test"; + +// BasePage - ALL pages extend this +export class BasePage { + constructor(protected page: Page) {} + + async goto(path: string): Promise { + await this.page.goto(path); + await this.page.waitForLoadState("networkidle"); + } + + // Common methods go here (see Refactoring Guidelines) + async waitForNotification(): Promise { + await this.page.waitForSelector('[role="status"]'); + } + + async verifyNotificationMessage(message: string): Promise { + const notification = this.page.locator('[role="status"]'); + await expect(notification).toContainText(message); + } +} + +// Page-specific implementation +export interface LoginData { + email: string; + password: string; +} + +export class LoginPage extends BasePage { + readonly emailInput: Locator; + readonly passwordInput: Locator; + readonly submitButton: Locator; + + constructor(page: Page) { + super(page); + this.emailInput = page.getByLabel("Email"); + this.passwordInput = page.getByLabel("Password"); + this.submitButton = page.getByRole("button", { name: "Sign in" }); + } + + async goto(): Promise { + await super.goto("/login"); + } + + async login(data: LoginData): Promise { + await this.emailInput.fill(data.email); + await this.passwordInput.fill(data.password); + await this.submitButton.click(); + } + + async verifyCriticalOutcome(): Promise { + await expect(this.page).toHaveURL("/dashboard"); + } +} +``` + +## Page Object Reuse (CRITICAL) + +**Always check existing page objects before creating new ones!** + +```typescript +// ✅ GOOD: Reuse existing page objects +import { SignInPage } from "../sign-in/sign-in-page"; +import { HomePage } from "../home/home-page"; + +test("User can sign up and login", async ({ page }) => { + const signUpPage = new SignUpPage(page); + const signInPage = new SignInPage(page); // REUSE + const homePage = new HomePage(page); // REUSE + + await signUpPage.signUp(userData); + await homePage.verifyPageLoaded(); // REUSE method + await homePage.signOut(); // REUSE method + await signInPage.login(credentials); // REUSE method +}); + +// ❌ BAD: Recreating existing functionality +export class SignUpPage extends BasePage { + async logout() { /* ... */ } // ❌ HomePage already has this + async login() { /* ... */ } // ❌ SignInPage already has this +} +``` + +**Guidelines:** +- Check `tests/` for existing page objects first +- Import and reuse existing pages +- Create page objects only when page doesn't exist +- If test requires multiple pages, ensure all page objects exist (create if needed) + +## Refactoring Guidelines + +### Move to `BasePage` when: +- ✅ Navigation helpers used by multiple pages (`waitForPageLoad()`, `getCurrentUrl()`) +- ✅ Common UI interactions (notifications, modals, theme toggles) +- ✅ Verification patterns repeated across pages (`isVisible()`, `waitForVisible()`) +- ✅ Error handling that applies to all pages +- ✅ Screenshot utilities for debugging + +### Move to `helpers.ts` when: +- ✅ Test data generation (`generateUniqueEmail()`, `generateTestUser()`) +- ✅ Setup/teardown utilities (`createTestUser()`, `cleanupTestData()`) +- ✅ Custom assertions (`expectNotificationToContain()`) +- ✅ API helpers for test setup (`seedDatabase()`, `resetState()`) +- ✅ Time utilities (`waitForCondition()`, `retryAction()`) + +**Before (BAD):** +```typescript +// Repeated in multiple page objects +export class SignUpPage extends BasePage { + async waitForNotification(): Promise { + await this.page.waitForSelector('[role="status"]'); + } +} +export class SignInPage extends BasePage { + async waitForNotification(): Promise { + await this.page.waitForSelector('[role="status"]'); // DUPLICATED! + } +} +``` + +**After (GOOD):** +```typescript +// BasePage - shared across all pages +export class BasePage { + async waitForNotification(): Promise { + await this.page.waitForSelector('[role="status"]'); + } +} + +// helpers.ts - data generation +export function generateUniqueEmail(): string { + return `test.${Date.now()}@example.com`; +} + +export function generateTestUser() { + return { + name: "Test User", + email: generateUniqueEmail(), + password: "TestPassword123!", + }; +} +``` + +## Test Pattern with Tags + +```typescript +import { test, expect } from "@playwright/test"; +import { LoginPage } from "./login-page"; + +test.describe("Login", () => { + test("User can login successfully", + { tag: ["@critical", "@e2e", "@login", "@LOGIN-E2E-001"] }, + async ({ page }) => { + const loginPage = new LoginPage(page); + + await loginPage.goto(); + await loginPage.login({ email: "user@test.com", password: "pass123" }); + + await expect(page).toHaveURL("/dashboard"); + } + ); +}); +``` + +**Tag Categories:** +- Priority: `@critical`, `@high`, `@medium`, `@low` +- Type: `@e2e` +- Feature: `@signup`, `@signin`, `@dashboard` +- Test ID: `@SIGNUP-E2E-001`, `@LOGIN-E2E-002` + +## Test Documentation Format ({page-name}.md) + +```markdown +### E2E Tests: {Feature Name} + +**Suite ID:** `{SUITE-ID}` +**Feature:** {Feature description} + +--- + +## Test Case: `{TEST-ID}` - {Test case title} + +**Priority:** `{critical|high|medium|low}` + +**Tags:** +- type → @e2e +- feature → @{feature-name} + +**Description/Objective:** {Brief description} + +**Preconditions:** +- {Prerequisites for test to run} +- {Required data or state} + +### Flow Steps: +1. {Step 1} +2. {Step 2} +3. {Step 3} + +### Expected Result: +- {Expected outcome 1} +- {Expected outcome 2} + +### Key verification points: +- {Assertion 1} +- {Assertion 2} + +### Notes: +- {Additional considerations} +``` + +**Documentation Rules:** +- ❌ NO general test running instructions +- ❌ NO file structure explanations +- ❌ NO code examples or tutorials +- ❌ NO troubleshooting sections +- ✅ Focus ONLY on specific test case +- ✅ Keep under 60 lines when possible + +## Commands + +```bash +npx playwright test # Run all +npx playwright test --grep "login" # Filter by name +npx playwright test --ui # Interactive UI +npx playwright test --debug # Debug mode +npx playwright test tests/login/ # Run specific folder +``` + +## Prowler-Specific Patterns + +For Prowler UI E2E testing with authentication setup, environment variables, and test IDs, see: +- **Documentation**: [references/prowler-e2e.md](references/prowler-e2e.md) diff --git a/skills/playwright/references/prowler-e2e.md b/skills/playwright/references/prowler-e2e.md new file mode 100644 index 0000000000..bc9efaf78e --- /dev/null +++ b/skills/playwright/references/prowler-e2e.md @@ -0,0 +1,16 @@ +# Prowler-Specific E2E Patterns + +## Local Documentation + +For Prowler-specific Playwright patterns, see: + +- `docs/developer-guide/end2end-testing.mdx` - Complete E2E testing guide + +## Contents + +The Prowler documentation covers patterns NOT in the generic playwright skill: +- Authentication setup projects (`admin.auth.setup`, `member.auth.setup`, etc.) +- Environment variables (`E2E_AWS_PROVIDER_ACCOUNT_ID`, etc.) +- Page Object location (`ui/tests/`) +- Test ID conventions (`@PROVIDER-E2E-001`, `@SCANS-E2E-001`) +- Serial test requirements for data-dependent tests diff --git a/skills/prowler-api/SKILL.md b/skills/prowler-api/SKILL.md new file mode 100644 index 0000000000..62f3c15156 --- /dev/null +++ b/skills/prowler-api/SKILL.md @@ -0,0 +1,137 @@ +--- +name: prowler-api +description: > + Prowler API patterns: RLS, RBAC, providers, Celery tasks. + Trigger: When working on api/ - models, serializers, views, filters, tasks. +license: Apache-2.0 +metadata: + author: prowler-cloud + version: "1.0" +allowed-tools: Read, Edit, Write, Glob, Grep, Bash, WebFetch, WebSearch, Task +--- + +## Critical Rules + +- ALWAYS use `rls_transaction(tenant_id)` when querying outside ViewSet context +- ALWAYS use `get_role()` before checking permissions (returns FIRST role only) +- NEVER access `Provider.objects` without RLS context in Celery tasks +- ALWAYS use `@set_tenant` then `@handle_provider_deletion` decorator order + +--- + +## 1. Providers (10 Supported) + +UID validation is dynamic: `getattr(self, f"validate_{self.provider}_uid")(self.uid)` + +| Provider | UID Format | Example | +|----------|-----------|---------| +| AWS | 12 digits | `123456789012` | +| Azure | UUID v4 | `a1b2c3d4-e5f6-...` | +| GCP | 6-30 chars, lowercase, letter start | `my-gcp-project` | +| M365 | Valid domain | `contoso.onmicrosoft.com` | +| Kubernetes | 2-251 chars | `arn:aws:eks:...` | +| GitHub | 1-39 chars | `my-org` | +| IaC | Git URL | `https://github.com/user/repo.git` | +| Oracle Cloud | OCID format | `ocid1.tenancy.oc1..` | +| MongoDB Atlas | 24-char hex | `507f1f77bcf86cd799439011` | +| Alibaba Cloud | 16 digits | `1234567890123456` | + +**Adding new provider**: Add to `ProviderChoices` enum + create `validate__uid()` staticmethod. + +--- + +## 2. Row-Level Security (RLS) + +```python +from api.db_utils import rls_transaction + +with rls_transaction(tenant_id): + providers = Provider.objects.filter(connected=True) + # PostgreSQL enforces tenant_id automatically +``` + +Models inherit from `RowLevelSecurityProtectedModel` with `RowLevelSecurityConstraint`. + +--- + +## 3. Managers + +```python +Provider.objects.all() # Only is_deleted=False +Provider.all_objects.all() # All including deleted +Finding.objects.all() # Only from active providers +``` + +--- + +## 4. RBAC + +```python +from api.rbac.permissions import get_role, get_providers, Permissions + +user_role = get_role(self.request.user) # Returns FIRST role only + +if user_role.unlimited_visibility: + queryset = Provider.objects.filter(tenant_id=tenant_id) +else: + queryset = get_providers(user_role) # Filtered by provider_groups +``` + +**Permissions**: `MANAGE_USERS`, `MANAGE_ACCOUNT`, `MANAGE_BILLING`, `MANAGE_PROVIDERS`, `MANAGE_INTEGRATIONS`, `MANAGE_SCANS`, `UNLIMITED_VISIBILITY` + +--- + +## 5. Celery Tasks + +```python +@shared_task(base=RLSTask, name="task-name", queue="scans") +@set_tenant +@handle_provider_deletion +def my_task(tenant_id: str, provider_id: str): + pass +``` + +**Queues**: Check `tasks/tasks.py`. Common: `scans`, `overview`, `compliance`, `integrations`. + +**Orchestration**: Use `chain()` for sequential, `group()` for parallel. + +--- + +## 6. JSON:API Format + +```python +content_type = "application/vnd.api+json" + +# Request +{"data": {"type": "providers", "attributes": {"provider": "aws", "uid": "123456789012"}}} + +# Response access +response.json()["data"]["attributes"]["alias"] +``` + +--- + +## 7. Serializers + +| Pattern | Usage | +|---------|-------| +| `ProviderSerializer` | Read (list/retrieve) | +| `ProviderCreateSerializer` | POST | +| `ProviderUpdateSerializer` | PATCH | +| `RLSSerializer` | Auto-injects tenant_id | + +--- + +## Commands + +```bash +cd api && poetry run python manage.py migrate # Run migrations +cd api && poetry run python manage.py shell # Django shell +cd api && poetry run celery -A config.celery worker -l info # Start worker +``` + +--- + +## Resources + +- **Documentation**: See [references/api-docs.md](references/api-docs.md) for local file paths and documentation diff --git a/skills/prowler-api/references/api-docs.md b/skills/prowler-api/references/api-docs.md new file mode 100644 index 0000000000..72bc7990fd --- /dev/null +++ b/skills/prowler-api/references/api-docs.md @@ -0,0 +1,21 @@ +# API Documentation + +## Local Documentation + +For API-related patterns, see: + +- `api/src/backend/api/models.py` - Models, Providers, UID validation +- `api/src/backend/api/v1/views.py` - ViewSets, RBAC patterns +- `api/src/backend/api/v1/serializers.py` - Serializers +- `api/src/backend/api/rbac/permissions.py` - RBAC functions +- `api/src/backend/tasks/tasks.py` - Celery tasks +- `api/src/backend/api/db_utils.py` - rls_transaction + +## Contents + +The documentation covers: +- Row-Level Security (RLS) implementation +- RBAC permission system +- Provider validation patterns +- Celery task orchestration +- JSON:API serialization format diff --git a/skills/prowler-compliance/SKILL.md b/skills/prowler-compliance/SKILL.md new file mode 100644 index 0000000000..8b82617d6e --- /dev/null +++ b/skills/prowler-compliance/SKILL.md @@ -0,0 +1,113 @@ +--- +name: prowler-compliance +description: > + Creates and manages Prowler compliance frameworks. + Trigger: When working with compliance frameworks (CIS, NIST, PCI-DSS, SOC2, GDPR). +license: Apache-2.0 +metadata: + author: prowler-cloud + version: "1.0" +allowed-tools: Read, Edit, Write, Glob, Grep, Bash, WebFetch, WebSearch, Task +--- + +## When to Use + +Use this skill when: +- Creating a new compliance framework for any provider +- Adding requirements to existing frameworks +- Mapping checks to compliance controls + +## Compliance Framework Structure + +Frameworks are JSON files in: `prowler/compliance/{provider}/{framework}.json` + +```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...", + "Requirements": [ + { + "Id": "1.1", + "Name": "Requirement name", + "Description": "Detailed description of the requirement", + "Attributes": [ + { + "Section": "1. Identity and Access Management", + "Profile": "Level 1", + "AssessmentStatus": "Automated", + "Description": "Attribute description" + } + ], + "Checks": ["check_name_1", "check_name_2"] + } + ] +} +``` + +## Supported Frameworks + +**Industry standards:** +- CIS (Center for Internet Security) +- NIST 800-53, NIST CSF +- CISA + +**Regulatory compliance:** +- PCI-DSS +- HIPAA +- GDPR +- FedRAMP +- SOC2 + +**Cloud-specific:** +- AWS Well-Architected Framework (Security Pillar) +- AWS Foundational Technical Review (FTR) +- Azure Security Benchmark +- GCP Security Best Practices + +## Framework Requirement Mapping + +Each requirement maps to one or more checks: + +```json +{ + "Id": "2.1.1", + "Name": "Ensure MFA is enabled for all IAM users", + "Description": "Multi-Factor Authentication adds an extra layer of protection...", + "Checks": [ + "iam_user_mfa_enabled", + "iam_root_mfa_enabled", + "iam_user_hardware_mfa_enabled" + ] +} +``` + +## Best Practices + +1. **Requirement IDs**: Follow the original framework numbering (e.g., "1.1", "2.3.4") +2. **Check Mapping**: Map to existing checks when possible, create new checks only if needed +3. **Completeness**: Include all framework requirements, even if no check exists (document as manual) +4. **Version Control**: Include framework version in the name and file + +## Commands + +```bash +# List available frameworks for a provider +poetry run python prowler-cli.py {provider} --list-compliance + +# Run scan with specific compliance framework +poetry run python prowler-cli.py {provider} --compliance {framework} + +# Run scan with multiple frameworks +poetry run python prowler-cli.py {provider} --compliance cis_aws_benchmark_v2 pci_dss_3.2.1 + +# Output compliance report +poetry run python prowler-cli.py {provider} --compliance {framework} -M csv json html +``` + +## Resources + +- **Templates**: See [assets/](assets/) for complete CIS framework JSON template +- **Documentation**: See [references/compliance-docs.md](references/compliance-docs.md) for official Prowler Developer Guide links diff --git a/skills/prowler-compliance/assets/cis_framework.json b/skills/prowler-compliance/assets/cis_framework.json new file mode 100644 index 0000000000..817c0ca6aa --- /dev/null +++ b/skills/prowler-compliance/assets/cis_framework.json @@ -0,0 +1,76 @@ +{ + "Framework": "CIS", + "Name": "CIS Amazon Web Services Foundations Benchmark v5.0.0", + "Version": "5.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.", + "Requirements": [ + { + "Id": "1.1", + "Description": "Maintain current contact details", + "Checks": [ + "account_maintain_current_contact_details" + ], + "Attributes": [ + { + "Section": "1 Identity and Access Management", + "Profile": "Level 1", + "AssessmentStatus": "Manual", + "Description": "Ensure contact email and telephone details for AWS accounts are current and map to more than one individual in your organization.", + "RationaleStatement": "If an AWS account is observed to be behaving in a prohibited or suspicious manner, AWS will attempt to contact the account owner by email and phone using the contact details listed.", + "ImpactStatement": "", + "RemediationProcedure": "This activity can only be performed via the AWS Console. Navigate to Account Settings and update contact information.", + "AuditProcedure": "This activity can only be performed via the AWS Console. Navigate to Account Settings and verify contact information is current.", + "AdditionalInformation": "", + "References": "https://docs.aws.amazon.com/accounts/latest/reference/manage-acct-update-contact.html", + "DefaultValue": "" + } + ] + }, + { + "Id": "1.3", + "Description": "Ensure no 'root' user account access key exists", + "Checks": [ + "iam_no_root_access_key" + ], + "Attributes": [ + { + "Section": "1 Identity and Access Management", + "Profile": "Level 1", + "AssessmentStatus": "Automated", + "Description": "The 'root' user account is the most privileged user in an AWS account. AWS Access Keys provide programmatic access to a given AWS account.", + "RationaleStatement": "Deleting access keys associated with the 'root' user account limits vectors by which the account can be compromised.", + "ImpactStatement": "", + "RemediationProcedure": "Navigate to IAM console, select root user, Security credentials tab, and delete any access keys.", + "AuditProcedure": "Run: aws iam get-account-summary | grep 'AccountAccessKeysPresent'", + "AdditionalInformation": "IAM User account root for us-gov cloud regions is not enabled by default.", + "References": "https://docs.aws.amazon.com/general/latest/gr/aws-access-keys-best-practices.html", + "DefaultValue": "" + } + ] + }, + { + "Id": "1.11", + "Description": "Ensure credentials unused for 45 days or more are disabled", + "Checks": [ + "iam_user_accesskey_unused", + "iam_user_console_access_unused" + ], + "Attributes": [ + { + "Section": "1 Identity and Access Management", + "Profile": "Level 1", + "AssessmentStatus": "Automated", + "Description": "AWS IAM users can access AWS resources using different types of credentials. It is recommended that all credentials unused for 45 days or more be deactivated or removed.", + "RationaleStatement": "Disabling or removing unnecessary credentials reduces the window of opportunity for compromised accounts.", + "ImpactStatement": "Users with deactivated credentials will lose access until re-enabled.", + "RemediationProcedure": "Use IAM console or CLI to deactivate unused access keys and remove unused passwords.", + "AuditProcedure": "Generate credential report and review password_last_used and access_key_last_used fields.", + "AdditionalInformation": "", + "References": "https://docs.aws.amazon.com/IAM/latest/UserGuide/id_credentials_getting-report.html", + "DefaultValue": "" + } + ] + } + ] +} diff --git a/skills/prowler-compliance/references/compliance-docs.md b/skills/prowler-compliance/references/compliance-docs.md new file mode 100644 index 0000000000..6d53252cfe --- /dev/null +++ b/skills/prowler-compliance/references/compliance-docs.md @@ -0,0 +1,15 @@ +# Compliance Framework Documentation + +## Local Documentation + +For detailed compliance framework patterns, see: + +- `docs/developer-guide/security-compliance-framework.mdx` - Complete guide for creating compliance frameworks (CIS, NIST, PCI-DSS, SOC2, GDPR) + +## Contents + +The documentation covers: +- Framework JSON structure +- Framework metadata (name, version, provider) +- Requirements array with IDs, descriptions, and attributes +- Check mappings for each requirement diff --git a/skills/prowler-docs/SKILL.md b/skills/prowler-docs/SKILL.md new file mode 100644 index 0000000000..d8fd8f9cb2 --- /dev/null +++ b/skills/prowler-docs/SKILL.md @@ -0,0 +1,122 @@ +--- +name: prowler-docs +description: > + Prowler documentation style guide and writing standards. + Trigger: When writing documentation for Prowler features, tutorials, or guides. +license: Apache-2.0 +metadata: + author: prowler-cloud + version: "1.0" +allowed-tools: Read, Edit, Write, Glob, Grep, Bash, WebFetch, WebSearch, Task +--- + +## When to Use + +Use this skill when writing Prowler documentation for: +- Feature documentation +- API/SDK references +- Tutorials and guides +- Release notes + +## Brand Voice + +### Unbiased Communication +- Avoid gendered pronouns (use "you/your" or "they/them") +- Use inclusive alternatives: businessman → businessperson, mankind → humanity +- No generalizations about gender, race, nationality, culture +- Avoid militaristic language: fight → address, kill chain → cyberattack chain + +### Technical Terminology +- Define key terms and acronyms on first use: "Identity and Access Management (IAM)" +- Prefer verbal over nominal constructions: "The report was created" not "The creation of the report" +- Use clear, accessible language; minimize jargon + +## Formatting Standards + +### Title Case Capitalization +Use Title Case for all headers: +- Good: "How to Configure Security Scanning" +- Bad: "How to configure security scanning" + +### Hyphenation +- Prenominal position: "world-leading company" +- Postnominal position: "features built in" + +### Bullet Points +Use when information can be logically divided: +```markdown +Prowler CLI includes: +* **Industry standards:** CIS, NIST 800, NIST CSF +* **Regulatory compliance:** RBI, FedRAMP, PCI-DSS +* **Privacy frameworks:** GDPR, HIPAA, FFIEC +``` + +### Interaction Verbs +- Desktop: Click, Double-click, Right-click, Drag, Scroll +- Touch: Tap, Double-tap, Press and hold, Swipe, Pinch + +## SEO Optimization + +### Sentence Structure +Place keywords at the beginning: +- Good: "To create a custom role, open a terminal..." +- Bad: "Open a terminal to create a custom role..." + +### Headers +- H1: Primary (unique, descriptive) +- H2-H6: Subheadings (logical hierarchy) +- Include keywords naturally + +## MDX Components + +### Version Badge +```mdx +import { VersionBadge } from "/snippets/version-badge.mdx" + +## New Feature Name + + + +Description of the feature... +``` + +### Warnings and Danger Calls +```mdx + +Disabling encryption may expose sensitive data to unauthorized access. + + + +Running this command will **permanently delete all data**. + +``` + +## Prowler Features (Proper Nouns) + +Reference without articles: +- Prowler App, Prowler CLI, Prowler SDK +- Prowler Cloud, Prowler Studio, Prowler Registry +- Built-in Compliance Checks +- Multi-cloud Security Scanning +- Autonomous Cloud Security Analyst (AI) + +## Documentation Structure + +``` +docs/ +├── getting-started/ +├── tutorials/ +├── providers/ +│ ├── aws/ +│ ├── azure/ +│ ├── gcp/ +│ └── ... +├── api/ +├── sdk/ +├── compliance/ +└── developer-guide/ +``` + +## Resources + +- **Documentation**: See [references/](references/) for links to local developer guide diff --git a/skills/prowler-docs/references/documentation-docs.md b/skills/prowler-docs/references/documentation-docs.md new file mode 100644 index 0000000000..727e4b0773 --- /dev/null +++ b/skills/prowler-docs/references/documentation-docs.md @@ -0,0 +1,15 @@ +# Documentation Style Guide + +## Local Documentation + +For documentation writing standards, see: + +- `docs/developer-guide/documentation.mdx` - Mintlify-based documentation and local development setup + +## Contents + +The documentation covers: +- Mintlify documentation system +- Local documentation development +- Style guide and conventions +- MDX file structure diff --git a/skills/prowler-mcp/SKILL.md b/skills/prowler-mcp/SKILL.md new file mode 100644 index 0000000000..4bf3eec7a0 --- /dev/null +++ b/skills/prowler-mcp/SKILL.md @@ -0,0 +1,78 @@ +--- +name: prowler-mcp +description: > + Creates MCP tools for Prowler MCP Server. Covers BaseTool pattern, model design, + and API client usage. Use when working on mcp_server/ directory. +license: Apache-2.0 +metadata: + author: prowler-cloud + version: "1.0" +allowed-tools: Read, Edit, Write, Glob, Grep, Bash, WebFetch, WebSearch, Task +--- + +## Overview + +The Prowler MCP Server uses three sub-servers with prefixed namespacing: + +| Sub-Server | Prefix | Auth | Purpose | +|------------|--------|------|---------| +| Prowler App | `prowler_app_*` | Required | Cloud management tools | +| Prowler Hub | `prowler_hub_*` | No | Security checks catalog | +| Prowler Docs | `prowler_docs_*` | No | Documentation search | + +For complete architecture, patterns, and examples, see [docs/developer-guide/mcp-server.mdx](../../../docs/developer-guide/mcp-server.mdx). + +--- + +## Critical Rules (Prowler App Only) + +### Tool Implementation + +- **ALWAYS**: Extend `BaseTool` (auto-registered via `tool_loader.py`, only public methods from the class are exposed as a tool) +- **NEVER**: Manually register BaseTool subclasses +- **NEVER**: Import tools directly in server.py + +### Models + +- **ALWAYS**: Use `MinimalSerializerMixin` for responses +- **ALWAYS**: Implement `from_api_response()` factory method +- **ALWAYS**: Use two-tier models (Simplified for lists, Detailed for single items) +- **NEVER**: Return raw API responses + +### API Client + +- **ALWAYS**: Use `self.api_client` singleton +- **ALWAYS**: Use `build_filter_params()` for query parameters +- **NEVER**: Create new httpx clients + +--- + +## Hub/Docs Tools + +Use `@mcp.tool()` decorator directly—no BaseTool or models required. + +--- + +## Quick Reference: New Prowler App Tool + +1. Create tool class in `prowler_app/tools/` extending `BaseTool` +2. Create models in `prowler_app/models/` using `MinimalSerializerMixin` +3. Tools auto-register via `tool_loader.py` + +--- + +## QA Checklist (Prowler App) + +- [ ] Tool docstrings describe LLM-relevant behavior +- [ ] Models use `MinimalSerializerMixin` +- [ ] API responses transformed to simplified models +- [ ] Error handling returns `{"error": str, "status": "failed"}` +- [ ] Parameters use `Field()` with descriptions +- [ ] No hardcoded secrets + +--- + +## Resources + +- **Full Guide**: [docs/developer-guide/mcp-server.mdx](../../../docs/developer-guide/mcp-server.mdx) +- **Templates**: See [assets/](assets/) for tool and model templates diff --git a/skills/prowler-mcp/assets/base_tool.py b/skills/prowler-mcp/assets/base_tool.py new file mode 100644 index 0000000000..7c176a22eb --- /dev/null +++ b/skills/prowler-mcp/assets/base_tool.py @@ -0,0 +1,62 @@ +# Example: BaseTool Abstract Class +# Source: mcp_server/prowler_mcp_server/prowler_app/tools/base.py + +import inspect +from abc import ABC +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from fastmcp import FastMCP + +from prowler_mcp_server.lib.logger import logger +from prowler_mcp_server.prowler_app.utils.api_client import ProwlerAPIClient + + +class BaseTool(ABC): + """ + Abstract base class for MCP tools. + + Key patterns: + 1. Auto-registers all public async methods as tools + 2. Provides shared api_client and logger via properties + 3. Subclasses just define async methods with Field() parameters + """ + + def __init__(self): + self._api_client = ProwlerAPIClient() + self._logger = logger + + @property + def api_client(self) -> ProwlerAPIClient: + """Shared API client for making authenticated requests.""" + return self._api_client + + @property + def logger(self): + """Logger for structured logging.""" + return self._logger + + def register_tools(self, mcp: "FastMCP") -> None: + """ + Auto-register all public async methods as MCP tools. + + Subclasses don't need to override this - just define async methods. + """ + registered_count = 0 + + for name, method in inspect.getmembers(self, predicate=inspect.ismethod): + # Skip private/protected methods + if name.startswith("_"): + continue + # Skip inherited methods + if name in ["register_tools", "api_client", "logger"]: + continue + # Only register async methods + if inspect.iscoroutinefunction(method): + mcp.tool(method) + registered_count += 1 + self.logger.debug(f"Auto-registered tool: {name}") + + self.logger.info( + f"Auto-registered {registered_count} tools from {self.__class__.__name__}" + ) diff --git a/skills/prowler-mcp/assets/models.py b/skills/prowler-mcp/assets/models.py new file mode 100644 index 0000000000..32669e6c84 --- /dev/null +++ b/skills/prowler-mcp/assets/models.py @@ -0,0 +1,146 @@ +# Example: MCP Models with MinimalSerializerMixin +# Source: mcp_server/prowler_mcp_server/prowler_app/models/ + +from typing import Any, Literal + +from pydantic import ( + BaseModel, + ConfigDict, + Field, + SerializerFunctionWrapHandler, + model_serializer, +) + + +class MinimalSerializerMixin(BaseModel): + """ + Mixin that excludes empty values from serialization. + + Key pattern: Reduces token usage by removing None, empty strings, empty lists/dicts. + Use this for all LLM-facing models. + """ + + @model_serializer(mode="wrap") + def _serialize(self, handler: SerializerFunctionWrapHandler) -> dict[str, Any]: + data = handler(self) + return {k: v for k, v in data.items() if not self._should_exclude(k, v)} + + def _should_exclude(self, key: str, value: Any) -> bool: + """Override in subclasses for custom exclusion logic.""" + if value is None: + return True + if value == "": + return True + if isinstance(value, list) and not value: + return True + if isinstance(value, dict) and not value: + return True + return False + + +class CheckRemediation(MinimalSerializerMixin, BaseModel): + """Remediation information - uses mixin to strip empty fields.""" + + model_config = ConfigDict(frozen=True) + + cli: str | None = Field(default=None, description="CLI command for remediation") + terraform: str | None = Field(default=None, description="Terraform code") + other: str | None = Field(default=None, description="Other remediation steps") + recommendation: str | None = Field( + default=None, description="Best practice recommendation" + ) + + +class SimplifiedFinding(MinimalSerializerMixin, BaseModel): + """ + Lightweight finding for list responses. + + Key pattern: Two-tier serialization + - SimplifiedFinding: minimal fields for lists (fast, low tokens) + - DetailedFinding: full fields for single item (complete info) + """ + + model_config = ConfigDict(frozen=True) + + id: str = Field(description="Finding UUID") + uid: str = Field(description="Unique finding identifier") + status: Literal["FAIL", "PASS", "MANUAL"] = Field(description="Finding status") + severity: str = Field(description="Severity level") + check_id: str = Field(description="Check ID that generated this finding") + resource_name: str | None = Field(default=None, description="Affected resource") + + @classmethod + def from_api_response(cls, data: dict) -> "SimplifiedFinding": + """Transform JSON:API response to model.""" + attributes = data["attributes"] + return cls( + id=data["id"], + uid=attributes["uid"], + status=attributes["status"], + severity=attributes["severity"], + check_id=attributes["check_id"], + resource_name=attributes.get("resource_name"), + ) + + +class DetailedFinding(SimplifiedFinding): + """ + Full finding details - extends SimplifiedFinding. + + Key pattern: Inheritance for two-tier serialization. + """ + + status_extended: str = Field(description="Detailed status message") + region: str | None = Field(default=None, description="Cloud region") + remediation: CheckRemediation | None = Field(default=None, description="How to fix") + + @classmethod + def from_api_response(cls, data: dict) -> "DetailedFinding": + """Transform JSON:API response to detailed model.""" + attributes = data["attributes"] + check_metadata = attributes.get("check_metadata", {}) + remediation_data = check_metadata.get("Remediation", {}) + + return cls( + id=data["id"], + uid=attributes["uid"], + status=attributes["status"], + severity=attributes["severity"], + check_id=attributes["check_id"], + resource_name=attributes.get("resource_name"), + status_extended=attributes.get("status_extended", ""), + region=attributes.get("region"), + remediation=( + CheckRemediation( + cli=remediation_data.get("Code", {}).get("CLI"), + terraform=remediation_data.get("Code", {}).get("Terraform"), + recommendation=remediation_data.get("Recommendation", {}).get( + "Text" + ), + ) + if remediation_data + else None + ), + ) + + +class FindingsListResponse(BaseModel): + """Wrapper for list responses with pagination.""" + + findings: list[SimplifiedFinding] + total: int + page: int + page_size: int + + @classmethod + def from_api_response(cls, data: dict) -> "FindingsListResponse": + findings = [ + SimplifiedFinding.from_api_response(f) for f in data.get("data", []) + ] + meta = data.get("meta", {}).get("pagination", {}) + return cls( + findings=findings, + total=meta.get("count", len(findings)), + page=meta.get("page", 1), + page_size=meta.get("page_size", len(findings)), + ) diff --git a/skills/prowler-mcp/assets/tool_implementation.py b/skills/prowler-mcp/assets/tool_implementation.py new file mode 100644 index 0000000000..87daa4b5e5 --- /dev/null +++ b/skills/prowler-mcp/assets/tool_implementation.py @@ -0,0 +1,95 @@ +# Example: Tool Implementation (FindingsTools) +# Source: mcp_server/prowler_mcp_server/prowler_app/tools/findings.py + +from typing import Any, Literal + +from prowler_mcp_server.prowler_app.models.findings import ( + DetailedFinding, + FindingsListResponse, +) +from prowler_mcp_server.prowler_app.tools.base import BaseTool +from pydantic import Field + + +class FindingsTools(BaseTool): + """ + MCP tools for security findings. + + Key patterns: + 1. Extends BaseTool (no need to override register_tools) + 2. Each async method becomes a tool automatically + 3. Use pydantic.Field() for parameter documentation + 4. Return dict from model_dump() for serialization + """ + + async def search_security_findings( + self, + severity: list[ + Literal["critical", "high", "medium", "low", "informational"] + ] = Field( + default=[], + description="Filter by severity levels. Multiple values allowed.", + ), + status: list[Literal["FAIL", "PASS", "MANUAL"]] = Field( + default=["FAIL"], + description="Filter by finding status. Default: ['FAIL'].", + ), + provider_type: list[str] = Field( + default=[], + description="Filter by cloud provider (aws, azure, gcp, etc.).", + ), + page_size: int = Field( + default=50, + description="Number of results per page.", + ), + page_number: int = Field( + default=1, + description="Page number (1-indexed).", + ), + ) -> dict[str, Any]: + """ + Search security findings with rich filtering. + + Returns simplified finding data optimized for LLM consumption. + """ + # Validate page size + self.api_client.validate_page_size(page_size) + + # Build query parameters + params = { + "page[size]": page_size, + "page[number]": page_number, + } + if severity: + params["filter[severity__in]"] = ",".join(severity) + if status: + params["filter[status__in]"] = ",".join(status) + if provider_type: + params["filter[provider_type__in]"] = ",".join(provider_type) + + # Make API request + api_response = await self.api_client.get("/findings", params=params) + + # Transform to simplified model and return + simplified_response = FindingsListResponse.from_api_response(api_response) + return simplified_response.model_dump() + + async def get_finding_details( + self, + finding_id: str = Field( + description="UUID of the finding to retrieve.", + ), + ) -> dict[str, Any]: + """ + Get comprehensive details for a specific finding. + + Returns full finding data including remediation steps. + """ + params = {"include": "resources,scan"} + api_response = await self.api_client.get( + f"/findings/{finding_id}", params=params + ) + detailed_finding = DetailedFinding.from_api_response( + api_response.get("data", {}) + ) + return detailed_finding.model_dump() diff --git a/skills/prowler-pr/SKILL.md b/skills/prowler-pr/SKILL.md new file mode 100644 index 0000000000..9d1e299a78 --- /dev/null +++ b/skills/prowler-pr/SKILL.md @@ -0,0 +1,130 @@ +--- +name: prowler-pr +description: > + Creates Pull Requests for Prowler following the project template and conventions. + Trigger: When user asks to create a PR, submit changes, or open a pull request. +license: Apache-2.0 +metadata: + author: prowler-cloud + version: "1.0" +allowed-tools: Read, Edit, Write, Glob, Grep, Bash, WebFetch, WebSearch, Task +--- + +## PR Creation Process + +1. **Analyze changes**: `git diff main...HEAD` to understand ALL commits +2. **Determine affected components**: SDK, API, UI, MCP, Docs +3. **Fill template sections** based on changes +4. **Create PR** with `gh pr create` + +## PR Template Structure + +```markdown +### Context + +{Why this change? Link issues with `Fix #XXXX`} + +### Description + +{Summary of changes and dependencies} + +### Steps to review + +{How to test/verify the changes} + +### Checklist + +
+ +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) + +
+ +- Are there new checks included in this PR? Yes / No + - If so, do we need to update permissions for the provider? +- [ ] Review if the code is being covered by tests. +- [ ] Review if code is being documented following 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 +- [ ] Ensure new entries are added to 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 (if applicable) +- [ ] All issue/task requirements work as expected on the UI +- [ ] Screenshots/Video - Mobile (X < 640px) +- [ ] Screenshots/Video - Tablet (640px > X < 1024px) +- [ ] Screenshots/Video - Desktop (X > 1024px) +- [ ] Ensure new entries are added to ui/CHANGELOG.md + +#### API (if applicable) +- [ ] 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. +- [ ] Ensure new entries are added to api/CHANGELOG.md + +### License + +By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license. +``` + +## Component-Specific Rules + +| Component | CHANGELOG | Extra Checks | +|-----------|-----------|--------------| +| SDK | `prowler/CHANGELOG.md` | New checks → permissions update? | +| API | `api/CHANGELOG.md` | API specs, version bump, endpoint output, EXPLAIN ANALYZE, performance | +| UI | `ui/CHANGELOG.md` | Screenshots for Mobile/Tablet/Desktop | +| MCP | `mcp_server/CHANGELOG.md` | N/A | + +## Commands + +```bash +# Check current branch status +git status +git log main..HEAD --oneline + +# View full diff +git diff main...HEAD + +# Create PR with heredoc for body +gh pr create --title "feat: description" --body "$(cat <<'EOF' +### Context +... +EOF +)" + +# Create draft PR +gh pr create --draft --title "feat: description" +``` + +## Title Conventions + +Follow conventional commits: +- `feat:` New feature +- `fix:` Bug fix +- `docs:` Documentation +- `chore:` Maintenance +- `refactor:` Code restructure +- `test:` Tests + +## Before Creating PR + +1. ✅ All tests pass locally +2. ✅ Linting passes (`make lint` or component-specific) +3. ✅ CHANGELOG updated (if applicable) +4. ✅ Branch is up to date with main +5. ✅ Commits are clean and descriptive + +## Resources + +- **Documentation**: See [references/](references/) for links to local developer guide diff --git a/skills/prowler-pr/references/pr-docs.md b/skills/prowler-pr/references/pr-docs.md new file mode 100644 index 0000000000..3fb1a7ce83 --- /dev/null +++ b/skills/prowler-pr/references/pr-docs.md @@ -0,0 +1,15 @@ +# Pull Request Documentation + +## Local Documentation + +For PR conventions and workflow, see: + +- `docs/developer-guide/introduction.mdx` - "Sending the Pull Request" section + +## Contents + +The documentation covers: +- PR template requirements +- Commit message conventions +- Review process +- CI/CD checks diff --git a/skills/prowler-provider/SKILL.md b/skills/prowler-provider/SKILL.md new file mode 100644 index 0000000000..493c393f1e --- /dev/null +++ b/skills/prowler-provider/SKILL.md @@ -0,0 +1,143 @@ +--- +name: prowler-provider +description: > + Creates new Prowler cloud providers or adds services to existing providers. + Trigger: When adding a new cloud provider or service to Prowler SDK. +license: Apache-2.0 +metadata: + author: prowler-cloud + version: "1.0" +allowed-tools: Read, Edit, Write, Glob, Grep, Bash, WebFetch, WebSearch, Task +--- + +## When to Use + +Use this skill when: +- Adding a new cloud provider to Prowler +- Adding a new service to an existing provider +- Understanding the provider architecture pattern + +## Provider Architecture Pattern + +Every provider MUST follow this structure: + +``` +prowler/providers/{provider}/ +├── __init__.py +├── {provider}_provider.py # Main provider class +├── models.py # Provider-specific models +├── config.py # Provider configuration +├── exceptions/ # Provider-specific exceptions +├── lib/ +│ ├── service/ # Base service class +│ ├── arguments/ # CLI arguments parser +│ └── mutelist/ # Mutelist functionality +└── services/ + └── {service}/ + ├── {service}_service.py # Resource fetcher + ├── {service}_client.py # Python singleton instance + └── {check_name}/ # Individual checks + ├── {check_name}.py + └── {check_name}.metadata.json +``` + +## Provider Class Template + +```python +from prowler.providers.common.provider import Provider + +class {Provider}Provider(Provider): + """Provider class for {Provider} cloud platform.""" + + def __init__(self, arguments): + super().__init__(arguments) + self.session = self._setup_session(arguments) + self.regions = self._get_regions() + + def _setup_session(self, arguments): + """Provider-specific authentication.""" + # Implement credential handling + pass + + def _get_regions(self): + """Get available regions for provider.""" + # Return list of regions + pass +``` + +## Service Class Template + +```python +from prowler.providers.{provider}.lib.service.service import {Provider}Service + +class {Service}({Provider}Service): + """Service class for {service} resources.""" + + def __init__(self, provider): + super().__init__(provider) + self.{resources} = [] + self._fetch_{resources}() + + def _fetch_{resources}(self): + """Fetch {resource} data from API.""" + try: + response = self.client.list_{resources}() + for item in response: + self.{resources}.append( + {Resource}( + id=item["id"], + name=item["name"], + region=item.get("region"), + ) + ) + except Exception as e: + logger.error(f"Error fetching {resources}: {e}") +``` + +## Service Client Template + +```python +from prowler.providers.{provider}.services.{service}.{service}_service import {Service} + +{service}_client = {Service} +``` + +## Supported Providers + +Current providers: +- AWS (Amazon Web Services) +- Azure (Microsoft Azure) +- GCP (Google Cloud Platform) +- Kubernetes +- GitHub +- M365 (Microsoft 365) +- OracleCloud (Oracle Cloud Infrastructure) +- AlibabaCloud +- MongoDB Atlas +- NHN (NHN Cloud) +- LLM (Language Model providers) +- IaC (Infrastructure as Code) + +## Commands + +```bash +# Run provider +poetry run python prowler-cli.py {provider} + +# List services for provider +poetry run python prowler-cli.py {provider} --list-services + +# List checks for provider +poetry run python prowler-cli.py {provider} --list-checks + +# Run specific service +poetry run python prowler-cli.py {provider} --services {service} + +# Debug mode +poetry run python prowler-cli.py {provider} --log-level DEBUG +``` + +## Resources + +- **Templates**: See [assets/](assets/) for Provider, Service, and Client singleton templates +- **Documentation**: See [references/provider-docs.md](references/provider-docs.md) for official Prowler Developer Guide links diff --git a/skills/prowler-provider/assets/client.py b/skills/prowler-provider/assets/client.py new file mode 100644 index 0000000000..ed47f5bc40 --- /dev/null +++ b/skills/prowler-provider/assets/client.py @@ -0,0 +1,56 @@ +# Example: Singleton Client Pattern +# Source: prowler/providers/github/services/repository/repository_client.py + +""" +Singleton Client Pattern + +This pattern is CRITICAL for how Prowler checks access service data. + +How it works: +1. When this module is imported, the service is instantiated ONCE +2. The service fetches all data during __init__ (eager loading) +3. All checks import this singleton and access pre-fetched data +4. No additional API calls needed during check execution + +File: prowler/providers/github/services/repository/repository_client.py +""" + +from prowler.providers.common.provider import Provider +from prowler.providers.github.services.repository.repository_service import Repository + +# SINGLETON: Instantiated once when module is first imported +# Provider.get_global_provider() returns the provider set in __init__ +repository_client = Repository(Provider.get_global_provider()) + + +""" +Usage in checks: + +from prowler.providers.github.services.repository.repository_client import ( + repository_client, +) + +class repository_secret_scanning_enabled(Check): + def execute(self): + findings = [] + for repo in repository_client.repositories.values(): + # Access pre-fetched repository data + report = CheckReportGithub(metadata=self.metadata(), resource=repo) + if repo.secret_scanning_enabled: + report.status = "PASS" + else: + report.status = "FAIL" + findings.append(report) + return findings +""" + + +# Another example for organization service +# File: prowler/providers/github/services/organization/organization_client.py + +# from prowler.providers.common.provider import Provider +# from prowler.providers.github.services.organization.organization_service import ( +# Organization, +# ) +# +# organization_client = Organization(Provider.get_global_provider()) diff --git a/skills/prowler-provider/assets/provider.py b/skills/prowler-provider/assets/provider.py new file mode 100644 index 0000000000..27bc8ff0b9 --- /dev/null +++ b/skills/prowler-provider/assets/provider.py @@ -0,0 +1,143 @@ +# Example: Provider Class Template (GitHub Provider) +# Source: prowler/providers/github/github_provider.py + + +from prowler.config.config import ( + default_config_file_path, + get_default_mute_file_path, + load_and_validate_config_file, +) +from prowler.lib.logger import logger +from prowler.lib.mutelist.mutelist import Mutelist +from prowler.providers.common.models import Audit_Metadata, Connection +from prowler.providers.common.provider import Provider + + +class GithubProvider(Provider): + """ + GitHub Provider - Template for creating new providers. + + Required attributes (from abstract Provider): + - _type: str - Provider identifier + - _session: Session model - Authentication credentials + - _identity: Identity model - Authenticated user info + - _audit_config: dict - Check configuration + - _mutelist: Mutelist - Finding filtering + """ + + _type: str = "github" + _auth_method: str = None + _session: "GithubSession" + _identity: "GithubIdentityInfo" + _audit_config: dict + _mutelist: Mutelist + audit_metadata: Audit_Metadata + + def __init__( + self, + # Authentication credentials + personal_access_token: str = "", + # Provider configuration + config_path: str = None, + config_content: dict = None, + fixer_config: dict = {}, + mutelist_path: str = None, + mutelist_content: dict = None, + # Provider scoping + repositories: list = None, + organizations: list = None, + ): + logger.info("Instantiating GitHub Provider...") + + # Store scoping configuration + self._repositories = repositories or [] + self._organizations = organizations or [] + + # Step 1: Setup session (authentication) + self._session = self.setup_session(personal_access_token) + self._auth_method = "Personal Access Token" + + # Step 2: Setup identity (who is authenticated) + self._identity = self.setup_identity(self._session) + + # Step 3: Load audit config + if config_content: + self._audit_config = config_content + else: + if not config_path: + config_path = default_config_file_path + self._audit_config = load_and_validate_config_file(self._type, config_path) + + # Step 4: Load fixer config + self._fixer_config = fixer_config + + # Step 5: Load mutelist + if mutelist_content: + self._mutelist = GithubMutelist(mutelist_content=mutelist_content) + else: + if not mutelist_path: + mutelist_path = get_default_mute_file_path(self.type) + self._mutelist = GithubMutelist(mutelist_path=mutelist_path) + + # CRITICAL: Register as global provider + Provider.set_global_provider(self) + + # Required property implementations + @property + def type(self) -> str: + return self._type + + @property + def session(self) -> "GithubSession": + return self._session + + @property + def identity(self) -> "GithubIdentityInfo": + return self._identity + + @property + def audit_config(self) -> dict: + return self._audit_config + + @property + def mutelist(self) -> Mutelist: + return self._mutelist + + @staticmethod + def setup_session(personal_access_token: str) -> "GithubSession": + """Create authenticated session from credentials.""" + if not personal_access_token: + raise ValueError("Personal access token required") + return GithubSession(token=personal_access_token) + + @staticmethod + def setup_identity(session: "GithubSession") -> "GithubIdentityInfo": + """Get identity info for authenticated user.""" + # Make API call to get user info + # g = Github(auth=Auth.Token(session.token)) + # user = g.get_user() + return GithubIdentityInfo( + account_id="user-id", + account_name="username", + account_url="https://github.com/username", + ) + + def print_credentials(self): + """Display credentials in CLI output.""" + print(f"GitHub Account: {self.identity.account_name}") + print(f"Auth Method: {self._auth_method}") + + @staticmethod + def test_connection( + personal_access_token: str = None, + raise_on_exception: bool = True, + ) -> Connection: + """Test if credentials can connect to the provider.""" + try: + session = GithubProvider.setup_session(personal_access_token) + GithubProvider.setup_identity(session) + return Connection(is_connected=True) + except Exception as e: + if raise_on_exception: + raise + return Connection(is_connected=False, error=str(e)) diff --git a/skills/prowler-provider/assets/service.py b/skills/prowler-provider/assets/service.py new file mode 100644 index 0000000000..0ad73a9142 --- /dev/null +++ b/skills/prowler-provider/assets/service.py @@ -0,0 +1,119 @@ +# Example: Service Base Class and Implementation +# Source: prowler/providers/github/lib/service/service.py +# Source: prowler/providers/github/services/repository/repository_service.py + +from typing import Optional + +from pydantic.v1 import BaseModel + +from prowler.lib.logger import logger + +# ============================================================ +# Base Service Class +# ============================================================ + + +class GithubService: + """ + Base service class for all GitHub services. + + Key patterns: + 1. Receives provider in __init__ + 2. Creates API clients in __set_clients__ + 3. Stores audit_config and fixer_config for check access + """ + + def __init__(self, service: str, provider: "GithubProvider"): + self.provider = provider + self.clients = self.__set_clients__(provider.session) + self.audit_config = provider.audit_config + self.fixer_config = provider.fixer_config + + def __set_clients__(self, session: "GithubSession") -> list: + """Create API clients based on authentication type.""" + clients = [] + try: + # Create client(s) based on session credentials + # For token auth: single client + # For GitHub App: multiple clients (one per installation) + pass + except Exception as error: + logger.error(f"{error.__class__.__name__}: {error}") + return clients + + +# ============================================================ +# Service Implementation +# ============================================================ + + +class Repository(GithubService): + """ + Repository service - fetches and stores repository data. + + Key patterns: + 1. Inherits from GithubService + 2. Fetches all data in __init__ (eager loading) + 3. Stores data in attributes for check access + 4. Defines Pydantic models for data structures + """ + + def __init__(self, provider: "GithubProvider"): + super().__init__(__class__.__name__, provider) + # Fetch and store data during initialization + self.repositories = self._list_repositories() + + def _list_repositories(self) -> dict: + """List repositories based on provider scoping.""" + logger.info("Repository - Listing Repositories...") + repos = {} + + try: + for client in self.clients: + # Get repos from specified repositories + for repo_name in self.provider.repositories: + repo = client.get_repo(repo_name) + self._process_repository(repo, repos) + + # Get repos from specified organizations + for org_name in self.provider.organizations: + org = client.get_organization(org_name) + for repo in org.get_repos(): + self._process_repository(repo, repos) + except Exception as error: + logger.error(f"{error.__class__.__name__}: {error}") + + return repos + + def _process_repository(self, repo, repos: dict): + """Process a single repository and add to repos dict.""" + repos[repo.id] = Repo( + id=repo.id, + name=repo.name, + owner=repo.owner.login, + full_name=repo.full_name, + private=repo.private, + archived=repo.archived, + ) + + +# ============================================================ +# Pydantic Models for Service Data +# ============================================================ + + +class Repo(BaseModel): + """Model for GitHub Repository.""" + + id: int + name: str + owner: str + full_name: str + private: bool + archived: bool + secret_scanning_enabled: Optional[bool] = None + dependabot_enabled: Optional[bool] = None + + class Config: + # Make model hashable for use as dict key + frozen = True diff --git a/skills/prowler-provider/references/provider-docs.md b/skills/prowler-provider/references/provider-docs.md new file mode 100644 index 0000000000..02261aaad0 --- /dev/null +++ b/skills/prowler-provider/references/provider-docs.md @@ -0,0 +1,28 @@ +# Provider Documentation + +## Local Documentation + +For detailed provider development patterns, see: + +### Core Documentation +- `docs/developer-guide/provider.mdx` - Provider architecture and creation guide +- `docs/developer-guide/services.mdx` - Adding services to existing providers + +### Provider-Specific Details +- `docs/developer-guide/aws-details.mdx` - AWS provider implementation +- `docs/developer-guide/azure-details.mdx` - Azure provider implementation +- `docs/developer-guide/gcp-details.mdx` - GCP provider implementation +- `docs/developer-guide/kubernetes-details.mdx` - Kubernetes provider implementation +- `docs/developer-guide/github-details.mdx` - GitHub provider implementation +- `docs/developer-guide/m365-details.mdx` - Microsoft 365 provider implementation +- `docs/developer-guide/alibabacloud-details.mdx` - Alibaba Cloud provider implementation +- `docs/developer-guide/llm-details.mdx` - LLM provider implementation + +## Contents + +The documentation covers: +- Provider types (SDK, API, Tool/Wrapper) +- Provider class structure and identity +- Service creation patterns +- Client singleton implementation +- Provider-specific authentication and API patterns diff --git a/skills/prowler-sdk-check/SKILL.md b/skills/prowler-sdk-check/SKILL.md new file mode 100644 index 0000000000..f032f6b590 --- /dev/null +++ b/skills/prowler-sdk-check/SKILL.md @@ -0,0 +1,257 @@ +--- +name: prowler-sdk-check +description: > + Creates Prowler security checks following SDK architecture patterns. + Trigger: When user asks to create a new security check for any provider (AWS, Azure, GCP, K8s, GitHub, etc.) +license: Apache-2.0 +metadata: + author: prowler-cloud + version: "1.0" +allowed-tools: Read, Edit, Write, Glob, Grep, Bash, WebFetch, WebSearch, Task +--- + +## Check Structure + +``` +prowler/providers/{provider}/services/{service}/{check_name}/ +├── __init__.py +├── {check_name}.py +└── {check_name}.metadata.json +``` + +--- + +## Step-by-Step Creation Process + +### 1. Prerequisites + +- **Verify check doesn't exist**: Search `prowler/providers/{provider}/services/{service}/` +- **Ensure provider and service exist** - create them first if not +- **Confirm service has required methods** - may need to add/modify service methods to get data + +### 2. Create Check Files + +```bash +mkdir -p prowler/providers/{provider}/services/{service}/{check_name} +touch prowler/providers/{provider}/services/{service}/{check_name}/__init__.py +touch prowler/providers/{provider}/services/{service}/{check_name}/{check_name}.py +touch prowler/providers/{provider}/services/{service}/{check_name}/{check_name}.metadata.json +``` + +### 3. Implement Check Logic + +```python +from prowler.lib.check.models import Check, Check_Report_{Provider} +from prowler.providers.{provider}.services.{service}.{service}_client import {service}_client + +class {check_name}(Check): + """Ensure that {resource} meets {security_requirement}.""" + def execute(self) -> list[Check_Report_{Provider}]: + """Execute the check logic. + + Returns: + A list of reports containing the result of the check. + """ + findings = [] + for resource in {service}_client.{resources}: + report = Check_Report_{Provider}(metadata=self.metadata(), resource=resource) + report.status = "PASS" if resource.is_compliant else "FAIL" + report.status_extended = f"Resource {resource.name} compliance status." + findings.append(report) + return findings +``` + +### 4. Create Metadata File + +See complete schema below and `assets/` folder for complete templates. +For detailed field documentation, see `references/metadata-docs.md`. + +### 5. Verify Check Detection + +```bash +poetry run python prowler-cli.py {provider} --list-checks | grep {check_name} +``` + +### 6. Run Check Locally + +```bash +poetry run python prowler-cli.py {provider} --log-level ERROR --verbose --check {check_name} +``` + +### 7. Create Tests + +See `prowler-test-sdk` skill for test patterns (PASS, FAIL, no resources, error handling). + +--- + +## Check Naming Convention + +``` +{service}_{resource}_{security_control} +``` + +Examples: +- `ec2_instance_public_ip_disabled` +- `s3_bucket_encryption_enabled` +- `iam_user_mfa_enabled` + +--- + +## Metadata Schema (COMPLETE) + +```json +{ + "Provider": "aws", + "CheckID": "{check_name}", + "CheckTitle": "Human-readable title", + "CheckType": [ + "Software and Configuration Checks/AWS Security Best Practices", + "Software and Configuration Checks/Industry and Regulatory Standards/AWS Foundational Security Best Practices" + ], + "ServiceName": "{service}", + "SubServiceName": "", + "ResourceIdTemplate": "", + "Severity": "low|medium|high|critical", + "ResourceType": "AwsEc2Instance|Other", + "ResourceGroup": "security|compute|storage|network", + "Description": "**Bold resource name**. Detailed explanation of what this check evaluates and why it matters.", + "Risk": "What happens if non-compliant. Explain attack vectors, data exposure risks, compliance impact.", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://docs.aws.amazon.com/..." + ], + "Remediation": { + "Code": { + "CLI": "aws {service} {command} --option value", + "NativeIaC": "```yaml\nResources:\n Resource:\n Type: AWS::{Service}::{Resource}\n Properties:\n Key: value # This line fixes the issue\n```", + "Other": "1. Console steps\n2. Step by step", + "Terraform": "```hcl\nresource \"aws_{service}_{resource}\" \"example\" {\n key = \"value\" # This line fixes the issue\n}\n```" + }, + "Recommendation": { + "Text": "Detailed recommendation for remediation.", + "Url": "https://hub.prowler.com/check/{check_name}" + } + }, + "Categories": [ + "identity-access", + "encryption", + "logging", + "forensics-ready", + "internet-exposed", + "trust-boundaries" + ], + "DependsOn": [], + "RelatedTo": [], + "Notes": "" +} +``` + +### Required Fields + +| Field | Description | +|-------|-------------| +| `Provider` | Provider name: aws, azure, gcp, kubernetes, github, m365 | +| `CheckID` | Must match class name and folder name | +| `CheckTitle` | Human-readable title | +| `Severity` | `low`, `medium`, `high`, `critical` | +| `ServiceName` | Service being checked | +| `Description` | What the check evaluates | +| `Risk` | Security impact of non-compliance | +| `Remediation.Code.CLI` | CLI fix command | +| `Remediation.Recommendation.Text` | How to fix | + +### Severity Guidelines + +| Severity | When to Use | +|----------|-------------| +| `critical` | Direct data exposure, RCE, privilege escalation | +| `high` | Significant security risk, compliance violation | +| `medium` | Defense-in-depth, best practice | +| `low` | Informational, minor hardening | + +--- + +## Check Report Statuses + +| Status | When to Use | +|--------|-------------| +| `PASS` | Resource is compliant | +| `FAIL` | Resource is non-compliant | +| `MANUAL` | Requires human verification | + +--- + +## Common Patterns + +### AWS Check with Regional Resources + +```python +from prowler.lib.check.models import Check, Check_Report_AWS +from prowler.providers.aws.services.s3.s3_client import s3_client + +class s3_bucket_encryption_enabled(Check): + def execute(self) -> list[Check_Report_AWS]: + findings = [] + for bucket in s3_client.buckets.values(): + report = Check_Report_AWS(metadata=self.metadata(), resource=bucket) + if bucket.encryption: + report.status = "PASS" + report.status_extended = f"S3 bucket {bucket.name} has encryption enabled." + else: + report.status = "FAIL" + report.status_extended = f"S3 bucket {bucket.name} does not have encryption enabled." + findings.append(report) + return findings +``` + +### Check with Multiple Conditions + +```python +from prowler.lib.check.models import Check, Check_Report_AWS +from prowler.providers.aws.services.ec2.ec2_client import ec2_client + +class ec2_instance_hardened(Check): + def execute(self) -> list[Check_Report_AWS]: + findings = [] + for instance in ec2_client.instances: + report = Check_Report_AWS(metadata=self.metadata(), resource=instance) + + issues = [] + if instance.public_ip: + issues.append("has public IP") + if not instance.metadata_options.http_tokens == "required": + issues.append("IMDSv2 not enforced") + + if issues: + report.status = "FAIL" + report.status_extended = f"Instance {instance.id} {', '.join(issues)}." + else: + report.status = "PASS" + report.status_extended = f"Instance {instance.id} is properly hardened." + + findings.append(report) + return findings +``` + +--- + +## Commands + +```bash +# Verify detection +poetry run python prowler-cli.py {provider} --list-checks | grep {check_name} + +# Run check +poetry run python prowler-cli.py {provider} --log-level ERROR --verbose --check {check_name} + +# Run with specific profile/credentials +poetry run python prowler-cli.py aws --profile myprofile --check {check_name} + +# Run multiple checks +poetry run python prowler-cli.py {provider} --check {check1} {check2} {check3} +``` + +## Resources + +- **Templates**: See [assets/](assets/) for complete check and metadata templates (AWS, Azure, GCP) +- **Documentation**: See [references/metadata-docs.md](references/metadata-docs.md) for official Prowler Developer Guide links diff --git a/skills/prowler-sdk-check/assets/aws_check.py b/skills/prowler-sdk-check/assets/aws_check.py new file mode 100644 index 0000000000..9dd1f5c109 --- /dev/null +++ b/skills/prowler-sdk-check/assets/aws_check.py @@ -0,0 +1,20 @@ +# Example: AWS S3 Bucket Encryption Check +# Source: prowler/providers/aws/services/s3/s3_bucket_default_encryption/ + +from prowler.lib.check.models import Check, Check_Report_AWS +from prowler.providers.aws.services.s3.s3_client import s3_client + + +class s3_bucket_default_encryption(Check): + def execute(self): + findings = [] + for bucket in s3_client.buckets.values(): + report = Check_Report_AWS(metadata=self.metadata(), resource=bucket) + if bucket.encryption: + report.status = "PASS" + report.status_extended = f"S3 Bucket {bucket.name} has Server Side Encryption with {bucket.encryption}." + else: + report.status = "FAIL" + report.status_extended = f"S3 Bucket {bucket.name} does not have Server Side Encryption enabled." + findings.append(report) + return findings diff --git a/skills/prowler-sdk-check/assets/aws_metadata.json b/skills/prowler-sdk-check/assets/aws_metadata.json new file mode 100644 index 0000000000..fbf1e657dd --- /dev/null +++ b/skills/prowler-sdk-check/assets/aws_metadata.json @@ -0,0 +1,35 @@ +{ + "Provider": "aws", + "CheckID": "s3_bucket_default_encryption", + "CheckTitle": "Check if S3 buckets have default encryption (SSE) enabled or use a bucket policy to enforce it.", + "CheckType": [ + "Data Protection" + ], + "ServiceName": "s3", + "SubServiceName": "", + "ResourceIdTemplate": "arn:partition:s3:::bucket_name", + "Severity": "medium", + "ResourceType": "AwsS3Bucket", + "ResourceGroup": "storage", + "Description": "Check if S3 buckets have default encryption (SSE) enabled or use a bucket policy to enforce it.", + "Risk": "Amazon S3 default encryption provides a way to set the default encryption behavior for an S3 bucket. This will ensure data-at-rest is encrypted.", + "RelatedUrl": "", + "Remediation": { + "Code": { + "CLI": "aws s3api put-bucket-encryption --bucket --server-side-encryption-configuration '{\"Rules\": [{\"ApplyServerSideEncryptionByDefault\": {\"SSEAlgorithm\": \"AES256\"}}]}'", + "NativeIaC": "https://docs.prowler.com/checks/aws/s3-policies/s3_14-data-encrypted-at-rest#cloudformation", + "Other": "", + "Terraform": "https://docs.prowler.com/checks/aws/s3-policies/s3_14-data-encrypted-at-rest#terraform" + }, + "Recommendation": { + "Text": "Ensure that S3 buckets have encryption at rest enabled.", + "Url": "https://aws.amazon.com/blogs/security/how-to-prevent-uploads-of-unencrypted-objects-to-amazon-s3/" + } + }, + "Categories": [ + "encryption" + ], + "DependsOn": [], + "RelatedTo": [], + "Notes": "" +} diff --git a/skills/prowler-sdk-check/assets/azure_check.py b/skills/prowler-sdk-check/assets/azure_check.py new file mode 100644 index 0000000000..f7e036e7a5 --- /dev/null +++ b/skills/prowler-sdk-check/assets/azure_check.py @@ -0,0 +1,25 @@ +# Example: Azure Storage Secure Transfer Check +# Source: prowler/providers/azure/services/storage/storage_secure_transfer_required_is_enabled/ + +from prowler.lib.check.models import Check, Check_Report_Azure +from prowler.providers.azure.services.storage.storage_client import storage_client + + +class storage_secure_transfer_required_is_enabled(Check): + def execute(self) -> list[Check_Report_Azure]: + findings = [] + for subscription, storage_accounts in storage_client.storage_accounts.items(): + for storage_account in storage_accounts: + report = Check_Report_Azure( + metadata=self.metadata(), resource=storage_account + ) + report.subscription = subscription + report.status = "PASS" + report.status_extended = f"Storage account {storage_account.name} from subscription {subscription} has secure transfer required enabled." + if not storage_account.enable_https_traffic_only: + report.status = "FAIL" + report.status_extended = f"Storage account {storage_account.name} from subscription {subscription} has secure transfer required disabled." + + findings.append(report) + + return findings diff --git a/skills/prowler-sdk-check/assets/azure_metadata.json b/skills/prowler-sdk-check/assets/azure_metadata.json new file mode 100644 index 0000000000..e278849592 --- /dev/null +++ b/skills/prowler-sdk-check/assets/azure_metadata.json @@ -0,0 +1,33 @@ +{ + "Provider": "azure", + "CheckID": "storage_secure_transfer_required_is_enabled", + "CheckTitle": "Ensure that all data transferred between clients and your Azure Storage account is encrypted using the HTTPS protocol.", + "CheckType": [], + "ServiceName": "storage", + "SubServiceName": "", + "ResourceIdTemplate": "", + "Severity": "medium", + "ResourceType": "AzureStorageAccount", + "ResourceGroup": "storage", + "Description": "Ensure that all data transferred between clients and your Azure Storage account is encrypted using the HTTPS protocol.", + "Risk": "Requests to the storage account sent outside of a secure connection can be eavesdropped", + "RelatedUrl": "", + "Remediation": { + "Code": { + "CLI": "az storage account update --name --https-only true", + "NativeIaC": "", + "Other": "https://www.trendmicro.com/cloudoneconformity/knowledge-base/azure/StorageAccounts/secure-transfer-required.html", + "Terraform": "https://docs.prowler.com/checks/azure/azure-networking-policies/ensure-that-storage-account-enables-secure-transfer" + }, + "Recommendation": { + "Text": "Enable data encryption in transit.", + "Url": "" + } + }, + "Categories": [ + "encryption" + ], + "DependsOn": [], + "RelatedTo": [], + "Notes": "" +} diff --git a/skills/prowler-sdk-check/assets/gcp_check.py b/skills/prowler-sdk-check/assets/gcp_check.py new file mode 100644 index 0000000000..164a2b0a3d --- /dev/null +++ b/skills/prowler-sdk-check/assets/gcp_check.py @@ -0,0 +1,29 @@ +# Example: GCP Cloud Storage Bucket Versioning Check +# Source: prowler/providers/gcp/services/cloudstorage/cloudstorage_bucket_versioning_enabled/ + +from prowler.lib.check.models import Check, Check_Report_GCP +from prowler.providers.gcp.services.cloudstorage.cloudstorage_client import ( + cloudstorage_client, +) + + +class cloudstorage_bucket_versioning_enabled(Check): + """Ensure Cloud Storage buckets have Object Versioning enabled.""" + + def execute(self) -> list[Check_Report_GCP]: + findings = [] + for bucket in cloudstorage_client.buckets: + report = Check_Report_GCP(metadata=self.metadata(), resource=bucket) + report.status = "FAIL" + report.status_extended = ( + f"Bucket {bucket.name} does not have Object Versioning enabled." + ) + + if bucket.versioning_enabled: + report.status = "PASS" + report.status_extended = ( + f"Bucket {bucket.name} has Object Versioning enabled." + ) + + findings.append(report) + return findings diff --git a/skills/prowler-sdk-check/assets/gcp_metadata.json b/skills/prowler-sdk-check/assets/gcp_metadata.json new file mode 100644 index 0000000000..b6e28b0e17 --- /dev/null +++ b/skills/prowler-sdk-check/assets/gcp_metadata.json @@ -0,0 +1,37 @@ +{ + "Provider": "gcp", + "CheckID": "cloudstorage_bucket_versioning_enabled", + "CheckTitle": "Cloud Storage buckets have Object Versioning enabled", + "CheckType": [], + "ServiceName": "cloudstorage", + "SubServiceName": "", + "ResourceIdTemplate": "", + "Severity": "medium", + "ResourceType": "storage.googleapis.com/Bucket", + "ResourceGroup": "storage", + "Description": "Google Cloud Storage buckets are evaluated to ensure that Object Versioning is enabled.", + "Risk": "Buckets without Object Versioning enabled cannot recover previous object versions after accidental deletion or overwrites.", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://www.trendmicro.com/cloudoneconformity/knowledge-base/gcp/CloudStorage/enable-versioning.html", + "https://cloud.google.com/storage/docs/object-versioning" + ], + "Remediation": { + "Code": { + "CLI": "gcloud storage buckets update gs:// --versioning", + "NativeIaC": "", + "Other": "1) Open Google Cloud Console -> Storage -> Buckets\n2) Select the bucket\n3) Click 'Edit bucket' -> 'Protection'\n4) Enable 'Object versioning'\n5) Save", + "Terraform": "resource \"google_storage_bucket\" \"example\" {\n versioning {\n enabled = true\n }\n}" + }, + "Recommendation": { + "Text": "Enable Object Versioning on Cloud Storage buckets to protect against accidental data loss.", + "Url": "https://hub.prowler.com/check/cloudstorage_bucket_versioning_enabled" + } + }, + "Categories": [ + "resilience" + ], + "DependsOn": [], + "RelatedTo": [], + "Notes": "" +} diff --git a/skills/prowler-sdk-check/references/metadata-docs.md b/skills/prowler-sdk-check/references/metadata-docs.md new file mode 100644 index 0000000000..63a702a9d0 --- /dev/null +++ b/skills/prowler-sdk-check/references/metadata-docs.md @@ -0,0 +1,19 @@ +# Check Documentation + +## Local Documentation + +For detailed check development patterns, see: + +- `docs/developer-guide/checks.mdx` - Complete guide for creating security checks +- `docs/developer-guide/check-metadata-guidelines.mdx` - Metadata writing standards and best practices +- `docs/developer-guide/configurable-checks.mdx` - Using audit_config for configurable checks +- `docs/developer-guide/renaming-checks.mdx` - Guidelines for renaming existing checks + +## Contents + +The documentation covers: +- Check structure and naming conventions +- Metadata schema and field descriptions +- Check implementation patterns per provider +- Configurable check parameters +- Check renaming procedures diff --git a/skills/prowler-test-api/SKILL.md b/skills/prowler-test-api/SKILL.md new file mode 100644 index 0000000000..b53eddc44b --- /dev/null +++ b/skills/prowler-test-api/SKILL.md @@ -0,0 +1,118 @@ +--- +name: prowler-test-api +description: > + Testing patterns for Prowler API: ViewSets, Celery tasks, RLS isolation, RBAC. + Trigger: When writing tests for api/ - viewsets, serializers, tasks, models. +license: Apache-2.0 +metadata: + author: prowler-cloud + version: "1.0" +allowed-tools: Read, Edit, Write, Glob, Grep, Bash, WebFetch, WebSearch, Task +--- + +## Critical Rules + +- ALWAYS use `response.json()["data"]` not `response.data` +- ALWAYS use `content_type = "application/vnd.api+json"` in requests +- ALWAYS test cross-tenant isolation with `other_tenant_provider` fixture +- NEVER skip RLS isolation tests when adding new endpoints + +--- + +## 1. JSON:API Format (Critical) + +```python +content_type = "application/vnd.api+json" + +payload = { + "data": { + "type": "providers", # Plural, kebab-case + "id": str(resource.id), # Required for PATCH + "attributes": {"alias": "updated"}, + } +} + +response.json()["data"]["attributes"]["alias"] +``` + +--- + +## 2. RLS Isolation Tests + +```python +def test_cross_tenant_access_denied(self, authenticated_client, other_tenant_provider): + """User cannot see resources from other tenants.""" + response = authenticated_client.get( + reverse("provider-detail", args=[other_tenant_provider.id]) + ) + assert response.status_code == status.HTTP_404_NOT_FOUND +``` + +--- + +## 3. RBAC Tests + +```python +def test_unlimited_visibility_sees_all(self, authenticated_client_admin, providers_fixture): + response = authenticated_client_admin.get(reverse("provider-list")) + assert len(response.json()["data"]) == len(providers_fixture) + +def test_limited_visibility_sees_only_assigned(self, authenticated_client_limited): + # User with unlimited_visibility=False sees only providers in their provider_groups + pass + +def test_permission_required(self, authenticated_client_readonly): + response = authenticated_client_readonly.post(reverse("provider-list"), ...) + assert response.status_code == status.HTTP_403_FORBIDDEN +``` + +--- + +## 4. Managers (objects vs all_objects) + +```python +def test_objects_excludes_deleted(self): + deleted_provider = Provider.objects.create(..., is_deleted=True) + assert deleted_provider not in Provider.objects.all() + assert deleted_provider in Provider.all_objects.all() +``` + +--- + +## 5. Celery Task Tests + +```python +@patch("tasks.tasks.perform_prowler_scan") +def test_task_success(self, mock_scan): + mock_scan.return_value = {"findings_count": 100} + result = perform_scan_task(tenant_id="...", scan_id="...", provider_id="...") + assert result["findings_count"] == 100 +``` + +--- + +## 6. Key Fixtures + +| Fixture | Description | +|---------|-------------| +| `create_test_user` | Session user (dev@prowler.com) | +| `tenants_fixture` | 3 tenants (2 with membership, 1 isolated) | +| `providers_fixture` | Providers in tenant 1 | +| `other_tenant_provider` | Provider in isolated tenant (RLS tests) | +| `authenticated_client` | Client with JWT for tenant 1 | + +--- + +## Commands + +```bash +cd api && poetry run pytest -x --tb=short +cd api && poetry run pytest -k "test_provider" +cd api && poetry run pytest -k "TestRBAC" +``` + +--- + +## Resources + +- **Documentation**: See [references/test-api-docs.md](references/test-api-docs.md) for local file paths and documentation diff --git a/skills/prowler-test-api/references/test-api-docs.md b/skills/prowler-test-api/references/test-api-docs.md new file mode 100644 index 0000000000..2aa3e0134f --- /dev/null +++ b/skills/prowler-test-api/references/test-api-docs.md @@ -0,0 +1,18 @@ +# API Test Documentation + +## Local Documentation + +For API testing patterns, see: + +- `api/src/backend/conftest.py` - All fixtures +- `api/src/backend/api/tests/` - API tests +- `api/src/backend/tasks/tests/` - Task tests + +## Contents + +The documentation covers: +- JSON:API format for requests/responses +- RLS isolation test patterns +- RBAC permission tests +- Celery task mocking +- Test fixtures and their usage diff --git a/skills/prowler-test-sdk/SKILL.md b/skills/prowler-test-sdk/SKILL.md new file mode 100644 index 0000000000..f9e08b5811 --- /dev/null +++ b/skills/prowler-test-sdk/SKILL.md @@ -0,0 +1,321 @@ +--- +name: prowler-test-sdk +description: > + Testing patterns for Prowler SDK (Python). + Trigger: When writing tests for checks, services, or providers. +license: Apache-2.0 +metadata: + author: prowler-cloud + version: "1.0" +allowed-tools: Read, Edit, Write, Glob, Grep, Bash, WebFetch, WebSearch, Task +--- + +> **Generic Patterns**: For base pytest patterns (fixtures, mocking, parametrize, markers), see the `pytest` skill. +> This skill covers **Prowler-specific** conventions only. +> +> **Full Documentation**: `docs/developer-guide/unit-testing.mdx` + +## CRITICAL: Provider-Specific Testing + +| Provider | Mocking Approach | Decorator | +|----------|------------------|-----------| +| **AWS** | `moto` library | `@mock_aws` | +| **Azure, GCP, K8s, others** | `MagicMock` | None | + +**NEVER use moto for non-AWS providers. NEVER use MagicMock for AWS.** + +--- + +## AWS Check Test Pattern + +```python +from unittest import mock +from boto3 import client +from moto import mock_aws +from tests.providers.aws.utils import AWS_REGION_US_EAST_1, set_mocked_aws_provider + + +class Test_{check_name}: + @mock_aws + def test_no_resources(self): + from prowler.providers.aws.services.{service}.{service}_service import {ServiceClass} + + aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1]) + + with mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ): + with mock.patch( + "prowler.providers.aws.services.{service}.{check_name}.{check_name}.{service}_client", + new={ServiceClass}(aws_provider), + ): + from prowler.providers.aws.services.{service}.{check_name}.{check_name} import ( + {check_name}, + ) + + check = {check_name}() + result = check.execute() + + assert len(result) == 0 + + @mock_aws + def test_{check_name}_pass(self): + # Setup AWS resources with moto + {service}_client = client("{service}", region_name=AWS_REGION_US_EAST_1) + # Create compliant resource... + + from prowler.providers.aws.services.{service}.{service}_service import {ServiceClass} + + aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1]) + + with mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ): + with mock.patch( + "prowler.providers.aws.services.{service}.{check_name}.{check_name}.{service}_client", + new={ServiceClass}(aws_provider), + ): + from prowler.providers.aws.services.{service}.{check_name}.{check_name} import ( + {check_name}, + ) + + check = {check_name}() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "PASS" + + @mock_aws + def test_{check_name}_fail(self): + # Setup AWS resources with moto + {service}_client = client("{service}", region_name=AWS_REGION_US_EAST_1) + # Create non-compliant resource... + + from prowler.providers.aws.services.{service}.{service}_service import {ServiceClass} + + aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1]) + + with mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ): + with mock.patch( + "prowler.providers.aws.services.{service}.{check_name}.{check_name}.{service}_client", + new={ServiceClass}(aws_provider), + ): + from prowler.providers.aws.services.{service}.{check_name}.{check_name} import ( + {check_name}, + ) + + check = {check_name}() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "FAIL" +``` + +> **Critical**: Always import the check INSIDE the mock.patch context to ensure proper client mocking. + +--- + +## Azure Check Test Pattern + +**NO moto decorator. Use MagicMock to mock the service client directly.** + +```python +from unittest import mock +from uuid import uuid4 + +from prowler.providers.azure.services.{service}.{service}_service import {ResourceModel} +from tests.providers.azure.azure_fixtures import ( + AZURE_SUBSCRIPTION_ID, + set_mocked_azure_provider, +) + + +class Test_{check_name}: + def test_no_resources(self): + {service}_client = mock.MagicMock + {service}_client.{resources} = {} + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_azure_provider(), + ), + mock.patch( + "prowler.providers.azure.services.{service}.{check_name}.{check_name}.{service}_client", + new={service}_client, + ), + ): + from prowler.providers.azure.services.{service}.{check_name}.{check_name} import ( + {check_name}, + ) + + check = {check_name}() + result = check.execute() + assert len(result) == 0 + + def test_{check_name}_pass(self): + resource_id = str(uuid4()) + resource_name = "Test Resource" + + {service}_client = mock.MagicMock + {service}_client.{resources} = { + AZURE_SUBSCRIPTION_ID: { + resource_id: {ResourceModel}( + id=resource_id, + name=resource_name, + location="westeurope", + # ... compliant attributes + ) + } + } + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_azure_provider(), + ), + mock.patch( + "prowler.providers.azure.services.{service}.{check_name}.{check_name}.{service}_client", + new={service}_client, + ), + ): + from prowler.providers.azure.services.{service}.{check_name}.{check_name} import ( + {check_name}, + ) + + check = {check_name}() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "PASS" + assert result[0].subscription == AZURE_SUBSCRIPTION_ID + assert result[0].resource_name == resource_name + + def test_{check_name}_fail(self): + resource_id = str(uuid4()) + resource_name = "Test Resource" + + {service}_client = mock.MagicMock + {service}_client.{resources} = { + AZURE_SUBSCRIPTION_ID: { + resource_id: {ResourceModel}( + id=resource_id, + name=resource_name, + location="westeurope", + # ... non-compliant attributes + ) + } + } + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_azure_provider(), + ), + mock.patch( + "prowler.providers.azure.services.{service}.{check_name}.{check_name}.{service}_client", + new={service}_client, + ), + ): + from prowler.providers.azure.services.{service}.{check_name}.{check_name} import ( + {check_name}, + ) + + check = {check_name}() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "FAIL" +``` + +--- + +## GCP/Kubernetes/Other Providers + +Follow the same MagicMock pattern as Azure: + +```python +from tests.providers.gcp.gcp_fixtures import set_mocked_gcp_provider, GCP_PROJECT_ID +from tests.providers.kubernetes.kubernetes_fixtures import set_mocked_kubernetes_provider +``` + +**Key difference**: Each provider has its own fixtures file with `set_mocked_{provider}_provider`. + +--- + +## Provider Fixtures Reference + +| Provider | Fixtures File | Key Constants | +|----------|---------------|---------------| +| AWS | `tests/providers/aws/utils.py` | `AWS_REGION_US_EAST_1`, `AWS_ACCOUNT_NUMBER` | +| Azure | `tests/providers/azure/azure_fixtures.py` | `AZURE_SUBSCRIPTION_ID` | +| GCP | `tests/providers/gcp/gcp_fixtures.py` | `GCP_PROJECT_ID` | +| K8s | `tests/providers/kubernetes/kubernetes_fixtures.py` | - | + +--- + +## Test File Structure + +``` +tests/providers/{provider}/services/{service}/ +├── {service}_service_test.py # Service tests +└── {check_name}/ + └── {check_name}_test.py # Check tests +``` + +--- + +## Required Test Scenarios + +Every check MUST test: + +| Scenario | Expected | +|----------|----------| +| Resource compliant | `status == "PASS"` | +| Resource non-compliant | `status == "FAIL"` | +| No resources | `len(results) == 0` | + +--- + +## Assertions to Include + +```python +# Always verify these +assert result[0].status == "PASS" # or "FAIL" +assert result[0].status_extended == "Expected message..." +assert result[0].resource_id == expected_id +assert result[0].resource_name == expected_name + +# Provider-specific +assert result[0].region == "us-east-1" # AWS +assert result[0].subscription == AZURE_SUBSCRIPTION_ID # Azure +assert result[0].project_id == GCP_PROJECT_ID # GCP +``` + +--- + +## Commands + +```bash +# All SDK tests +poetry run pytest -n auto -vvv tests/ + +# Specific provider +poetry run pytest tests/providers/{provider}/ -v + +# Specific check +poetry run pytest tests/providers/{provider}/services/{service}/{check_name}/ -v + +# Stop on first failure +poetry run pytest -x tests/ +``` + +## Resources + +- **Templates**: See [assets/](assets/) for complete test templates (AWS with moto, Azure/GCP with MagicMock) +- **Documentation**: See [references/testing-docs.md](references/testing-docs.md) for official Prowler Developer Guide links diff --git a/skills/prowler-test-sdk/assets/aws_test.py b/skills/prowler-test-sdk/assets/aws_test.py new file mode 100644 index 0000000000..2137c5651f --- /dev/null +++ b/skills/prowler-test-sdk/assets/aws_test.py @@ -0,0 +1,149 @@ +# Example: AWS KMS Key Rotation Test +# Source: tests/providers/aws/services/kms/kms_cmk_rotation_enabled/ + +from unittest import mock + +import pytest +from boto3 import client +from moto import mock_aws + +from tests.providers.aws.utils import AWS_REGION_US_EAST_1, set_mocked_aws_provider + + +class Test_kms_cmk_rotation_enabled: + @mock_aws + def test_kms_no_key(self): + """Test when no KMS keys exist.""" + from prowler.providers.aws.services.kms.kms_service import KMS + + aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1]) + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ), + mock.patch( + "prowler.providers.aws.services.kms.kms_cmk_rotation_enabled.kms_cmk_rotation_enabled.kms_client", + new=KMS(aws_provider), + ), + ): + from prowler.providers.aws.services.kms.kms_cmk_rotation_enabled.kms_cmk_rotation_enabled import ( + kms_cmk_rotation_enabled, + ) + + check = kms_cmk_rotation_enabled() + result = check.execute() + + assert len(result) == 0 + + @mock_aws + def test_kms_cmk_rotation_enabled(self): + """Test PASS: KMS key with rotation enabled.""" + # Create mocked AWS resources using boto3 + kms_client = client("kms", region_name=AWS_REGION_US_EAST_1) + key = kms_client.create_key()["KeyMetadata"] + kms_client.enable_key_rotation(KeyId=key["KeyId"]) + + from prowler.providers.aws.services.kms.kms_service import KMS + + aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1]) + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ), + mock.patch( + "prowler.providers.aws.services.kms.kms_cmk_rotation_enabled.kms_cmk_rotation_enabled.kms_client", + new=KMS(aws_provider), + ), + ): + from prowler.providers.aws.services.kms.kms_cmk_rotation_enabled.kms_cmk_rotation_enabled import ( + kms_cmk_rotation_enabled, + ) + + check = kms_cmk_rotation_enabled() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "PASS" + assert result[0].resource_id == key["KeyId"] + assert result[0].resource_arn == key["Arn"] + + @mock_aws + def test_kms_cmk_rotation_disabled(self): + """Test FAIL: KMS key without rotation enabled.""" + kms_client = client("kms", region_name=AWS_REGION_US_EAST_1) + key = kms_client.create_key()["KeyMetadata"] + # Note: rotation NOT enabled + + from prowler.providers.aws.services.kms.kms_service import KMS + + aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1]) + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ), + mock.patch( + "prowler.providers.aws.services.kms.kms_cmk_rotation_enabled.kms_cmk_rotation_enabled.kms_client", + new=KMS(aws_provider), + ), + ): + from prowler.providers.aws.services.kms.kms_cmk_rotation_enabled.kms_cmk_rotation_enabled import ( + kms_cmk_rotation_enabled, + ) + + check = kms_cmk_rotation_enabled() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "FAIL" + assert result[0].resource_id == key["KeyId"] + + @pytest.mark.parametrize( + "no_of_keys_created,expected_no_of_passes", + [ + (5, 3), + (7, 5), + (10, 8), + ], + ) + @mock_aws + def test_kms_rotation_parametrized( + self, no_of_keys_created: int, expected_no_of_passes: int + ) -> None: + """Parametrized test demonstrating multiple scenarios.""" + kms_client = client("kms", region_name=AWS_REGION_US_EAST_1) + + for i in range(no_of_keys_created): + key = kms_client.create_key()["KeyMetadata"] + if i not in [2, 4]: # Skip enabling rotation for some keys + kms_client.enable_key_rotation(KeyId=key["KeyId"]) + + from prowler.providers.aws.services.kms.kms_service import KMS + + aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1]) + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ), + mock.patch( + "prowler.providers.aws.services.kms.kms_cmk_rotation_enabled.kms_cmk_rotation_enabled.kms_client", + new=KMS(aws_provider), + ), + ): + from prowler.providers.aws.services.kms.kms_cmk_rotation_enabled.kms_cmk_rotation_enabled import ( + kms_cmk_rotation_enabled, + ) + + check = kms_cmk_rotation_enabled() + result = check.execute() + + assert len(result) == no_of_keys_created + statuses = [r.status for r in result] + assert statuses.count("PASS") == expected_no_of_passes diff --git a/skills/prowler-test-sdk/assets/azure_test.py b/skills/prowler-test-sdk/assets/azure_test.py new file mode 100644 index 0000000000..4bf85d65e6 --- /dev/null +++ b/skills/prowler-test-sdk/assets/azure_test.py @@ -0,0 +1,137 @@ +# Example: Azure Storage Network Access Rule Test +# Source: tests/providers/azure/services/storage/storage_default_network_access_rule_is_denied/ + +from unittest import mock +from uuid import uuid4 + +from prowler.providers.azure.services.storage.storage_service import ( + Account, + NetworkRuleSet, +) +from tests.providers.azure.azure_fixtures import ( + AZURE_SUBSCRIPTION_ID, + set_mocked_azure_provider, +) + + +class Test_storage_default_network_access_rule_is_denied: + def test_storage_no_storage_accounts(self): + """Test when no storage accounts exist.""" + storage_client = mock.MagicMock + storage_client.storage_accounts = {} + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_azure_provider(), + ), + mock.patch( + "prowler.providers.azure.services.storage.storage_default_network_access_rule_is_denied.storage_default_network_access_rule_is_denied.storage_client", + new=storage_client, + ), + ): + from prowler.providers.azure.services.storage.storage_default_network_access_rule_is_denied.storage_default_network_access_rule_is_denied import ( + storage_default_network_access_rule_is_denied, + ) + + check = storage_default_network_access_rule_is_denied() + result = check.execute() + assert len(result) == 0 + + def test_storage_network_access_rule_allowed(self): + """Test FAIL: Network access rule set to Allow.""" + storage_account_id = str(uuid4()) + storage_account_name = "Test Storage Account" + storage_client = mock.MagicMock + storage_client.storage_accounts = { + AZURE_SUBSCRIPTION_ID: [ + Account( + id=storage_account_id, + name=storage_account_name, + resouce_group_name="rg", + enable_https_traffic_only=False, + infrastructure_encryption=False, + allow_blob_public_access=False, + network_rule_set=NetworkRuleSet( + bypass="AzureServices", default_action="Allow" + ), + encryption_type="None", + minimum_tls_version="TLS1_2", + key_expiration_period_in_days=None, + location="westeurope", + private_endpoint_connections=[], + ) + ] + } + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_azure_provider(), + ), + mock.patch( + "prowler.providers.azure.services.storage.storage_default_network_access_rule_is_denied.storage_default_network_access_rule_is_denied.storage_client", + new=storage_client, + ), + ): + from prowler.providers.azure.services.storage.storage_default_network_access_rule_is_denied.storage_default_network_access_rule_is_denied import ( + storage_default_network_access_rule_is_denied, + ) + + check = storage_default_network_access_rule_is_denied() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "FAIL" + assert result[0].subscription == AZURE_SUBSCRIPTION_ID + assert result[0].resource_name == storage_account_name + assert result[0].resource_id == storage_account_id + assert result[0].location == "westeurope" + + def test_storage_network_access_rule_denied(self): + """Test PASS: Network access rule set to Deny.""" + storage_account_id = str(uuid4()) + storage_account_name = "Test Storage Account" + storage_client = mock.MagicMock + storage_client.storage_accounts = { + AZURE_SUBSCRIPTION_ID: [ + Account( + id=storage_account_id, + name=storage_account_name, + resouce_group_name="rg", + enable_https_traffic_only=False, + infrastructure_encryption=False, + allow_blob_public_access=False, + network_rule_set=NetworkRuleSet( + default_action="Deny", bypass="AzureServices" + ), + encryption_type="None", + minimum_tls_version="TLS1_2", + key_expiration_period_in_days=None, + location="westeurope", + private_endpoint_connections=[], + ) + ] + } + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_azure_provider(), + ), + mock.patch( + "prowler.providers.azure.services.storage.storage_default_network_access_rule_is_denied.storage_default_network_access_rule_is_denied.storage_client", + new=storage_client, + ), + ): + from prowler.providers.azure.services.storage.storage_default_network_access_rule_is_denied.storage_default_network_access_rule_is_denied import ( + storage_default_network_access_rule_is_denied, + ) + + check = storage_default_network_access_rule_is_denied() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "PASS" + assert result[0].subscription == AZURE_SUBSCRIPTION_ID + assert result[0].resource_name == storage_account_name diff --git a/skills/prowler-test-sdk/assets/gcp_test.py b/skills/prowler-test-sdk/assets/gcp_test.py new file mode 100644 index 0000000000..9f71a82121 --- /dev/null +++ b/skills/prowler-test-sdk/assets/gcp_test.py @@ -0,0 +1,126 @@ +# Example: GCP Cloud Storage Bucket Public Access Test +# Source: tests/providers/gcp/services/cloudstorage/cloudstorage_bucket_public_access/ + +from unittest import mock + +from tests.providers.gcp.gcp_fixtures import ( + GCP_PROJECT_ID, + GCP_US_CENTER1_LOCATION, + set_mocked_gcp_provider, +) + + +class TestCloudStorageBucketPublicAccess: + def test_bucket_public_access(self): + """Test FAIL: Bucket is publicly accessible.""" + cloudstorage_client = mock.MagicMock() + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_gcp_provider(), + ), + mock.patch( + "prowler.providers.gcp.services.cloudstorage.cloudstorage_bucket_public_access.cloudstorage_bucket_public_access.cloudstorage_client", + new=cloudstorage_client, + ), + ): + from prowler.providers.gcp.services.cloudstorage.cloudstorage_bucket_public_access.cloudstorage_bucket_public_access import ( + cloudstorage_bucket_public_access, + ) + from prowler.providers.gcp.services.cloudstorage.cloudstorage_service import ( + Bucket, + ) + + cloudstorage_client.project_ids = [GCP_PROJECT_ID] + cloudstorage_client.region = GCP_US_CENTER1_LOCATION + + cloudstorage_client.buckets = [ + Bucket( + name="example-bucket", + id="example-bucket", + region=GCP_US_CENTER1_LOCATION, + uniform_bucket_level_access=True, + public=True, + project_id=GCP_PROJECT_ID, + ) + ] + + check = cloudstorage_bucket_public_access() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "FAIL" + assert result[0].resource_id == "example-bucket" + assert result[0].resource_name == "example-bucket" + assert result[0].location == GCP_US_CENTER1_LOCATION + assert result[0].project_id == GCP_PROJECT_ID + + def test_bucket_no_public_access(self): + """Test PASS: Bucket is not publicly accessible.""" + cloudstorage_client = mock.MagicMock() + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_gcp_provider(), + ), + mock.patch( + "prowler.providers.gcp.services.cloudstorage.cloudstorage_bucket_public_access.cloudstorage_bucket_public_access.cloudstorage_client", + new=cloudstorage_client, + ), + ): + from prowler.providers.gcp.services.cloudstorage.cloudstorage_bucket_public_access.cloudstorage_bucket_public_access import ( + cloudstorage_bucket_public_access, + ) + from prowler.providers.gcp.services.cloudstorage.cloudstorage_service import ( + Bucket, + ) + + cloudstorage_client.project_ids = [GCP_PROJECT_ID] + cloudstorage_client.region = GCP_US_CENTER1_LOCATION + + cloudstorage_client.buckets = [ + Bucket( + name="example-bucket", + id="example-bucket", + region=GCP_US_CENTER1_LOCATION, + uniform_bucket_level_access=True, + public=False, + project_id=GCP_PROJECT_ID, + ) + ] + + check = cloudstorage_bucket_public_access() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "PASS" + assert result[0].resource_id == "example-bucket" + + def test_no_buckets(self): + """Test when no buckets exist.""" + cloudstorage_client = mock.MagicMock() + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_gcp_provider(), + ), + mock.patch( + "prowler.providers.gcp.services.cloudstorage.cloudstorage_bucket_public_access.cloudstorage_bucket_public_access.cloudstorage_client", + new=cloudstorage_client, + ), + ): + from prowler.providers.gcp.services.cloudstorage.cloudstorage_bucket_public_access.cloudstorage_bucket_public_access import ( + cloudstorage_bucket_public_access, + ) + + cloudstorage_client.project_ids = [GCP_PROJECT_ID] + cloudstorage_client.region = GCP_US_CENTER1_LOCATION + cloudstorage_client.buckets = [] + + check = cloudstorage_bucket_public_access() + result = check.execute() + + assert len(result) == 0 diff --git a/skills/prowler-test-sdk/references/testing-docs.md b/skills/prowler-test-sdk/references/testing-docs.md new file mode 100644 index 0000000000..75ec547372 --- /dev/null +++ b/skills/prowler-test-sdk/references/testing-docs.md @@ -0,0 +1,17 @@ +# SDK Testing Documentation + +## Local Documentation + +For detailed SDK testing patterns, see: + +- `docs/developer-guide/unit-testing.mdx` - Complete guide for writing check tests + +## Contents + +The documentation covers: +- AWS testing with moto (`@mock_aws` decorator) +- Azure testing with MagicMock +- GCP testing with MagicMock +- Provider-specific fixtures (`set_mocked_aws_provider`, etc.) +- Service dependency table for CI optimization +- Test structure and required scenarios diff --git a/skills/prowler-test-ui/SKILL.md b/skills/prowler-test-ui/SKILL.md new file mode 100644 index 0000000000..fbff0f9310 --- /dev/null +++ b/skills/prowler-test-ui/SKILL.md @@ -0,0 +1,190 @@ +--- +name: prowler-test-ui +description: > + E2E testing patterns for Prowler UI (Playwright). + Trigger: When writing E2E tests for the Next.js frontend. +license: Apache-2.0 +metadata: + author: prowler-cloud + version: "1.0" +allowed-tools: Read, Edit, Write, Glob, Grep, Bash, WebFetch, WebSearch, Task +--- + +> **Generic Patterns**: For base Playwright patterns (Page Object Model, selectors, helpers), see the `playwright` skill. +> This skill covers **Prowler-specific** conventions only. + +## Prowler UI Test Structure + +``` +ui/tests/ +├── base-page.ts # Prowler-specific base page +├── helpers.ts # Prowler test utilities +└── {page-name}/ + ├── {page-name}-page.ts # Page Object Model + ├── {page-name}.spec.ts # ALL tests (single file per feature) + └── {page-name}.md # Test documentation +``` + +--- + +## MCP Workflow - CRITICAL + +**⚠️ MANDATORY: If Playwright MCP tools are available, ALWAYS use them BEFORE creating tests.** + +1. **Navigate** to target page +2. **Take snapshot** to see actual DOM structure +3. **Interact** with forms/elements to verify real flow +4. **Document actual selectors** from snapshots +5. **Only then** write test code + +**Why**: Prevents tests based on assumptions. Real exploration = stable tests. + +--- + +## Prowler Base Page + +```typescript +import { Page, Locator, expect } from "@playwright/test"; + +export class BasePage { + constructor(protected page: Page) {} + + async goto(path: string): Promise { + await this.page.goto(path); + await this.page.waitForLoadState("networkidle"); + } + + async waitForPageLoad(): Promise { + await this.page.waitForLoadState("networkidle"); + } + + // Prowler-specific: notification handling + async waitForNotification(): Promise { + const notification = this.page.locator('[role="status"]'); + await notification.waitFor({ state: "visible" }); + return notification; + } + + async verifyNotificationMessage(message: string): Promise { + const notification = await this.waitForNotification(); + await expect(notification).toContainText(message); + } +} +``` + +--- + +## Prowler-Specific Pages + +### Providers Page + +```typescript +import { BasePage } from "../base-page"; + +export class ProvidersPage extends BasePage { + readonly addButton = this.page.getByRole("button", { name: "Add Provider" }); + readonly providerTable = this.page.getByRole("table"); + + async goto(): Promise { + await super.goto("/providers"); + } + + async addProvider(type: string, alias: string): Promise { + await this.addButton.click(); + await this.page.getByLabel("Provider Type").selectOption(type); + await this.page.getByLabel("Alias").fill(alias); + await this.page.getByRole("button", { name: "Create" }).click(); + } +} +``` + +### Scans Page + +```typescript +export class ScansPage extends BasePage { + readonly newScanButton = this.page.getByRole("button", { name: "New Scan" }); + readonly scanTable = this.page.getByRole("table"); + + async goto(): Promise { + await super.goto("/scans"); + } + + async startScan(providerAlias: string): Promise { + await this.newScanButton.click(); + await this.page.getByRole("combobox", { name: "Provider" }).click(); + await this.page.getByRole("option", { name: providerAlias }).click(); + await this.page.getByRole("button", { name: "Start Scan" }).click(); + } +} +``` + +--- + +## Test Tags for Prowler + +```typescript +test("Provider CRUD operations", + { tag: ["@critical", "@e2e", "@providers", "@PROV-E2E-001"] }, + async ({ page }) => { + // ... + } +); +``` + +| Category | Tags | +|----------|------| +| Priority | `@critical`, `@high`, `@medium`, `@low` | +| Type | `@e2e`, `@smoke`, `@regression` | +| Feature | `@providers`, `@scans`, `@findings`, `@compliance`, `@signin`, `@signup` | +| Test ID | `@PROV-E2E-001`, `@SCAN-E2E-002` | + +--- + +## Prowler Test Documentation Template + +**Keep under 60 lines. Focus on flow, preconditions, expected results only.** + +```markdown +### E2E Tests: {Feature Name} + +**Suite ID:** `{SUITE-ID}` +**Feature:** {Feature description} + +--- + +## Test Case: `{TEST-ID}` - {Test case title} + +**Priority:** `{critical|high|medium|low}` +**Tags:** @e2e, @{feature-name} + +**Preconditions:** +- {Prerequisites} + +### Flow Steps: +1. {Step} +2. {Step} + +### Expected Result: +- {Outcome} + +### Key Verification Points: +- {Assertion} +``` + +--- + +## Commands + +```bash +cd ui && pnpm run test:e2e # All tests +cd ui && pnpm run test:e2e tests/providers/ # Specific folder +cd ui && pnpm run test:e2e --grep "provider" # By pattern +cd ui && pnpm run test:e2e:ui # With UI +cd ui && pnpm run test:e2e:debug # Debug mode +cd ui && pnpm run test:e2e:headed # See browser +cd ui && pnpm run test:e2e:report # Generate report +``` + +## Resources + +- **Documentation**: See [references/](references/) for links to local developer guide diff --git a/skills/prowler-test-ui/references/e2e-docs.md b/skills/prowler-test-ui/references/e2e-docs.md new file mode 100644 index 0000000000..3ea6f4db47 --- /dev/null +++ b/skills/prowler-test-ui/references/e2e-docs.md @@ -0,0 +1,17 @@ +# E2E Testing Documentation + +## Local Documentation + +For Playwright E2E testing patterns, see: + +- `docs/developer-guide/end2end-testing.mdx` - Complete E2E testing guide + +## Contents + +The documentation covers: +- Playwright setup and configuration +- Page Object Model patterns +- Authentication states (`admin.auth.setup`, etc.) +- Environment variables (`E2E_*`) +- Test tagging conventions (`@PROVIDER-E2E-001`) +- Serial test requirements diff --git a/skills/prowler-ui/SKILL.md b/skills/prowler-ui/SKILL.md new file mode 100644 index 0000000000..0c9e338e91 --- /dev/null +++ b/skills/prowler-ui/SKILL.md @@ -0,0 +1,210 @@ +--- +name: prowler-ui +description: > + Prowler UI-specific patterns. For generic patterns, see: typescript, react-19, nextjs-15, tailwind-4. + Trigger: When working on ui/ directory - components, pages, actions, hooks. +license: Apache-2.0 +metadata: + author: prowler-cloud + version: "1.0" +allowed-tools: Read, Edit, Write, Glob, Grep, Bash, WebFetch, WebSearch, Task +--- + +## Related Generic Skills + +- `typescript` - Const types, flat interfaces +- `react-19` - No useMemo/useCallback, compiler +- `nextjs-15` - App Router, Server Actions +- `tailwind-4` - cn() utility, styling rules +- `zod-4` - Schema validation +- `zustand-5` - State management +- `ai-sdk-5` - Chat/AI features +- `playwright` - E2E testing (see also `prowler-test-ui`) + +## Tech Stack (Versions) + +``` +Next.js 15.5.9 | React 19.2.2 | Tailwind 4.1.13 | shadcn/ui +Zod 4.1.11 | React Hook Form 7.62.0 | Zustand 5.0.8 +NextAuth 5.0.0-beta.30 | Recharts 2.15.4 +HeroUI 2.8.4 (LEGACY - do not add new components) +``` + +## CRITICAL: Component Library Rule + +- **ALWAYS**: Use `shadcn/ui` + Tailwind (`components/shadcn/`) +- **NEVER**: Add new HeroUI components (`components/ui/` is legacy only) + +## DECISION TREES + +### Component Placement + +``` +New feature UI? → shadcn/ui + Tailwind +Existing HeroUI feature? → Keep HeroUI (don't mix) +Used 1 feature? → features/{feature}/components/ +Used 2+ features? → components/shared/ +Needs state/hooks? → "use client" +Server component? → No directive needed +``` + +### Code Location + +``` +Server action → actions/{feature}/{feature}.ts +Data transform → actions/{feature}/{feature}.adapter.ts +Types (shared 2+) → types/{domain}.ts +Types (local 1) → {feature}/types.ts +Utils (shared 2+) → lib/ +Utils (local 1) → {feature}/utils/ +Hooks (shared 2+) → hooks/ +Hooks (local 1) → {feature}/hooks.ts +shadcn components → components/shadcn/ +HeroUI components → components/ui/ (LEGACY) +``` + +### Styling Decision + +``` +Tailwind class exists? → className +Dynamic value? → style prop +Conditional styles? → cn() +Static only? → className (no cn()) +Recharts/library? → CHART_COLORS constant + var() +``` + +### Scope Rule (ABSOLUTE) + +- Used 2+ places → `lib/` or `types/` or `hooks/` (components go in `components/{domain}/`) +- Used 1 place → keep local in feature directory +- **This determines ALL folder structure decisions** + +## Project Structure + +``` +ui/ +├── app/ +│ ├── (auth)/ # Auth pages (login, signup) +│ └── (prowler)/ # Main app +│ ├── compliance/ +│ ├── findings/ +│ ├── providers/ +│ ├── scans/ +│ ├── services/ +│ └── integrations/ +├── components/ +│ ├── shadcn/ # shadcn/ui (USE THIS) +│ ├── ui/ # HeroUI (LEGACY) +│ ├── {domain}/ # Domain-specific (compliance, findings, providers, etc.) +│ ├── filters/ # Filter components +│ ├── graphs/ # Chart components +│ └── icons/ # Icon components +├── actions/ # Server actions +├── types/ # Shared types +├── hooks/ # Shared hooks +├── lib/ # Utilities +├── store/ # Zustand state +├── tests/ # Playwright E2E +└── styles/ # Global CSS +``` + +## Recharts (Special Case) + +For Recharts props that don't accept className: + +```typescript +const CHART_COLORS = { + primary: "var(--color-primary)", + secondary: "var(--color-secondary)", + text: "var(--color-text)", + gridLine: "var(--color-border)", +}; + +// Only use var() for library props, NEVER in className + + +``` + +## Form + Validation Pattern + +```typescript +"use client"; +import { useForm } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { z } from "zod"; + +const schema = z.object({ + email: z.email(), // Zod 4 syntax + name: z.string().min(1), +}); + +type FormData = z.infer; + +export function MyForm() { + const { register, handleSubmit, formState: { errors } } = useForm({ + resolver: zodResolver(schema), + }); + + const onSubmit = async (data: FormData) => { + await serverAction(data); + }; + + return ( +
+ + {errors.email && {errors.email.message}} + +
+ ); +} +``` + +## Commands + +```bash +# Development +cd ui && pnpm install +cd ui && pnpm run dev + +# Code Quality +cd ui && pnpm run typecheck +cd ui && pnpm run lint:fix +cd ui && pnpm run format:write +cd ui && pnpm run healthcheck # typecheck + lint + +# Testing +cd ui && pnpm run test:e2e +cd ui && pnpm run test:e2e:ui +cd ui && pnpm run test:e2e:debug + +# Build +cd ui && pnpm run build +cd ui && pnpm start +``` + +## QA Checklist Before Commit + +- [ ] `pnpm run typecheck` passes +- [ ] `pnpm run lint:fix` passes +- [ ] `pnpm run format:write` passes +- [ ] Relevant E2E tests pass +- [ ] All UI states handled (loading, error, empty) +- [ ] No secrets in code (use `.env.local`) +- [ ] Error messages sanitized (no stack traces to users) +- [ ] Server-side validation present (don't trust client) +- [ ] Accessibility: keyboard navigation, ARIA labels +- [ ] Mobile responsive (if applicable) + +## Migrations Reference + +| From | To | Key Changes | +|------|-----|-------------| +| React 18 | 19.1 | Async components, React Compiler (no useMemo/useCallback) | +| Next.js 14 | 15.5 | Improved App Router, better streaming | +| NextUI | HeroUI 2.8.4 | Package rename only, same API | +| Zod 3 | 4 | `z.email()` not `z.string().email()`, `error` not `message` | +| AI SDK 4 | 5 | `@ai-sdk/react`, `sendMessage` not `handleSubmit`, `parts` not `content` | + +## Resources + +- **Documentation**: See [references/](references/) for links to local developer guide diff --git a/skills/prowler-ui/references/ui-docs.md b/skills/prowler-ui/references/ui-docs.md new file mode 100644 index 0000000000..81efd37ea8 --- /dev/null +++ b/skills/prowler-ui/references/ui-docs.md @@ -0,0 +1,14 @@ +# UI Documentation + +## Local Documentation + +For UI-related patterns, see: + +- `docs/developer-guide/lighthouse.mdx` - AI agent integration and Lighthouse patterns + +## Contents + +The documentation covers: +- AI agent integration in the UI +- Lighthouse performance patterns +- Component optimization diff --git a/skills/prowler/SKILL.md b/skills/prowler/SKILL.md new file mode 100644 index 0000000000..bb520fe42b --- /dev/null +++ b/skills/prowler/SKILL.md @@ -0,0 +1,63 @@ +--- +name: prowler +description: > + Main entry point for Prowler development - quick reference for all components. + Trigger: General Prowler development questions, project overview, component navigation. +license: Apache-2.0 +metadata: + author: prowler-cloud + version: "1.0" +allowed-tools: Read, Edit, Write, Glob, Grep, Bash, WebFetch, WebSearch, Task +--- + +## Components + +| Component | Stack | Location | +|-----------|-------|----------| +| SDK | Python 3.9+, Poetry | `prowler/` | +| API | Django 5.1, DRF, Celery | `api/` | +| UI | Next.js 15, React 19, Tailwind 4 | `ui/` | +| MCP | FastMCP 2.13.1 | `mcp_server/` | + +## Quick Commands + +```bash +# SDK +poetry install --with dev +poetry run python prowler-cli.py aws --check check_name +poetry run pytest tests/ + +# API +cd api && poetry run python src/backend/manage.py runserver +cd api && poetry run pytest + +# UI +cd ui && pnpm run dev +cd ui && pnpm run healthcheck + +# MCP +cd mcp_server && uv run prowler-mcp + +# Full Stack +docker-compose up -d +``` + +## Providers + +AWS, Azure, GCP, Kubernetes, GitHub, M365, OCI, AlibabaCloud, MongoDB Atlas, IaC + +## Commit Style + +`feat:`, `fix:`, `docs:`, `chore:`, `perf:`, `refactor:`, `test:` + +## Related Skills + +- `prowler-sdk-check` - Create security checks +- `prowler-api` - Django/DRF patterns +- `prowler-ui` - Next.js/React patterns +- `prowler-mcp` - MCP server tools +- `prowler-test` - Testing patterns + +## Resources + +- **Documentation**: See [references/](references/) for links to local developer guide diff --git a/skills/prowler/references/prowler-docs.md b/skills/prowler/references/prowler-docs.md new file mode 100644 index 0000000000..1b09c832af --- /dev/null +++ b/skills/prowler/references/prowler-docs.md @@ -0,0 +1,14 @@ +# Prowler Documentation + +## Local Documentation + +For project overview and development setup, see: + +- `docs/developer-guide/introduction.mdx` - Repository structure, setup, and development environment + +## Contents + +The documentation covers: +- Project structure overview +- Development environment setup +- Repository conventions diff --git a/skills/pytest/SKILL.md b/skills/pytest/SKILL.md new file mode 100644 index 0000000000..bb2328cc41 --- /dev/null +++ b/skills/pytest/SKILL.md @@ -0,0 +1,192 @@ +--- +name: pytest +description: > + Pytest testing patterns for Python. + Trigger: When writing Python tests - fixtures, mocking, markers. +license: Apache-2.0 +metadata: + author: prowler-cloud + version: "1.0" +allowed-tools: Read, Edit, Write, Glob, Grep, Bash, WebFetch, WebSearch, Task +--- + +## Basic Test Structure + +```python +import pytest + +class TestUserService: + def test_create_user_success(self): + user = create_user(name="John", email="john@test.com") + assert user.name == "John" + assert user.email == "john@test.com" + + def test_create_user_invalid_email_fails(self): + with pytest.raises(ValueError, match="Invalid email"): + create_user(name="John", email="invalid") +``` + +## Fixtures + +```python +import pytest + +@pytest.fixture +def user(): + """Create a test user.""" + return User(name="Test User", email="test@example.com") + +@pytest.fixture +def authenticated_client(client, user): + """Client with authenticated user.""" + client.force_login(user) + return client + +# Fixture with teardown +@pytest.fixture +def temp_file(): + path = Path("/tmp/test_file.txt") + path.write_text("test content") + yield path # Test runs here + path.unlink() # Cleanup after test + +# Fixture scopes +@pytest.fixture(scope="module") # Once per module +@pytest.fixture(scope="class") # Once per class +@pytest.fixture(scope="session") # Once per test session +``` + +## conftest.py + +```python +# tests/conftest.py - Shared fixtures +import pytest + +@pytest.fixture +def db_session(): + session = create_session() + yield session + session.rollback() + +@pytest.fixture +def api_client(): + return TestClient(app) +``` + +## Mocking + +```python +from unittest.mock import patch, MagicMock + +class TestPaymentService: + def test_process_payment_success(self): + with patch("services.payment.stripe_client") as mock_stripe: + mock_stripe.charge.return_value = {"id": "ch_123", "status": "succeeded"} + + result = process_payment(amount=100) + + assert result["status"] == "succeeded" + mock_stripe.charge.assert_called_once_with(amount=100) + + def test_process_payment_failure(self): + with patch("services.payment.stripe_client") as mock_stripe: + mock_stripe.charge.side_effect = PaymentError("Card declined") + + with pytest.raises(PaymentError): + process_payment(amount=100) + +# MagicMock for complex objects +def test_with_mock_object(): + mock_user = MagicMock() + mock_user.id = "user-123" + mock_user.name = "Test User" + mock_user.is_active = True + + result = get_user_info(mock_user) + assert result["name"] == "Test User" +``` + +## Parametrize + +```python +@pytest.mark.parametrize("input,expected", [ + ("hello", "HELLO"), + ("world", "WORLD"), + ("pytest", "PYTEST"), +]) +def test_uppercase(input, expected): + assert input.upper() == expected + +@pytest.mark.parametrize("email,is_valid", [ + ("user@example.com", True), + ("invalid-email", False), + ("", False), + ("user@.com", False), +]) +def test_email_validation(email, is_valid): + assert validate_email(email) == is_valid +``` + +## Markers + +```python +# pytest.ini or pyproject.toml +[tool.pytest.ini_options] +markers = [ + "slow: marks tests as slow", + "integration: marks integration tests", +] + +# Usage +@pytest.mark.slow +def test_large_data_processing(): + ... + +@pytest.mark.integration +def test_database_connection(): + ... + +@pytest.mark.skip(reason="Not implemented yet") +def test_future_feature(): + ... + +@pytest.mark.skipif(sys.platform == "win32", reason="Unix only") +def test_unix_specific(): + ... + +# Run specific markers +# pytest -m "not slow" +# pytest -m "integration" +``` + +## Async Tests + +```python +import pytest + +@pytest.mark.asyncio +async def test_async_function(): + result = await async_fetch_data() + assert result is not None +``` + +## Commands + +```bash +pytest # Run all tests +pytest -v # Verbose output +pytest -x # Stop on first failure +pytest -k "test_user" # Filter by name +pytest -m "not slow" # Filter by marker +pytest --cov=src # With coverage +pytest -n auto # Parallel (pytest-xdist) +pytest --tb=short # Short traceback +``` + +## References + +For general pytest documentation, see: +- **Official Docs**: https://docs.pytest.org/en/stable/ + +For Prowler SDK testing with provider-specific patterns (moto, MagicMock), see: +- **Documentation**: [references/prowler-testing.md](references/prowler-testing.md) diff --git a/skills/pytest/references/prowler-testing.md b/skills/pytest/references/prowler-testing.md new file mode 100644 index 0000000000..c26c8104dc --- /dev/null +++ b/skills/pytest/references/prowler-testing.md @@ -0,0 +1,16 @@ +# Prowler-Specific Testing Patterns + +## Local Documentation + +For Prowler-specific pytest patterns, see: + +- `docs/developer-guide/unit-testing.mdx` - Complete SDK testing guide + +## Contents + +The Prowler documentation covers patterns NOT in the generic pytest skill: +- `set_mocked_aws_provider()` fixture pattern +- `@mock_aws` decorator usage with moto +- `mock_make_api_call` pattern +- Service dependency table for CI optimization +- Provider-specific mocking (AWS uses moto, Azure/GCP use MagicMock) diff --git a/skills/react-19/SKILL.md b/skills/react-19/SKILL.md new file mode 100644 index 0000000000..53ab6b0aea --- /dev/null +++ b/skills/react-19/SKILL.md @@ -0,0 +1,122 @@ +--- +name: react-19 +description: > + React 19 patterns with React Compiler. + Trigger: When writing React components - no useMemo/useCallback needed. +license: Apache-2.0 +metadata: + author: prowler-cloud + version: "1.0" +allowed-tools: Read, Edit, Write, Glob, Grep, Bash, WebFetch, WebSearch, Task +--- + +## No Manual Memoization (REQUIRED) + +```typescript +// ✅ React Compiler handles optimization automatically +function Component({ items }) { + const filtered = items.filter(x => x.active); + const sorted = filtered.sort((a, b) => a.name.localeCompare(b.name)); + + const handleClick = (id) => { + console.log(id); + }; + + return ; +} + +// ❌ NEVER: Manual memoization +const filtered = useMemo(() => items.filter(x => x.active), [items]); +const handleClick = useCallback((id) => console.log(id), []); +``` + +## Imports (REQUIRED) + +```typescript +// ✅ ALWAYS: Named imports +import { useState, useEffect, useRef } from "react"; + +// ❌ NEVER +import React from "react"; +import * as React from "react"; +``` + +## Server Components First + +```typescript +// ✅ Server Component (default) - no directive +export default async function Page() { + const data = await fetchData(); + return ; +} + +// ✅ Client Component - only when needed +"use client"; +export function Interactive() { + const [state, setState] = useState(false); + return ; +} +``` + +## When to use "use client" + +- useState, useEffect, useRef, useContext +- Event handlers (onClick, onChange) +- Browser APIs (window, localStorage) + +## use() Hook + +```typescript +import { use } from "react"; + +// Read promises (suspends until resolved) +function Comments({ promise }) { + const comments = use(promise); + return comments.map(c =>
{c.text}
); +} + +// Conditional context (not possible with useContext!) +function Theme({ showTheme }) { + if (showTheme) { + const theme = use(ThemeContext); + return
Themed
; + } + return
Plain
; +} +``` + +## Actions & useActionState + +```typescript +"use server"; +async function submitForm(formData: FormData) { + await saveToDatabase(formData); + revalidatePath("/"); +} + +// With pending state +import { useActionState } from "react"; + +function Form() { + const [state, action, isPending] = useActionState(submitForm, null); + return ( +
+ +
+ ); +} +``` + +## ref as Prop (No forwardRef) + +```typescript +// ✅ React 19: ref is just a prop +function Input({ ref, ...props }) { + return ; +} + +// ❌ Old way (unnecessary now) +const Input = forwardRef((props, ref) => ); +``` diff --git a/skills/setup.sh b/skills/setup.sh new file mode 100755 index 0000000000..8f0fc57197 --- /dev/null +++ b/skills/setup.sh @@ -0,0 +1,119 @@ +#!/bin/bash +# Setup AI Skills for Prowler development +# Configures AI coding assistants that follow agentskills.io standard: +# - Claude Code: .claude/skills/ symlink (auto-discovery) +# - Gemini CLI: .gemini/skills/ symlink (auto-discovery) +# - Codex (OpenAI): .codex/skills/ symlink + AGENTS.md +# - GitHub Copilot: reads AGENTS.md from repo root (no symlink needed) + +set -e + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(dirname "$SCRIPT_DIR")" +SKILLS_SOURCE="$SCRIPT_DIR" + +# Target locations +CLAUDE_SKILLS_TARGET="$REPO_ROOT/.claude/skills" +CODEX_SKILLS_TARGET="$REPO_ROOT/.codex/skills" +GEMINI_SKILLS_TARGET="$REPO_ROOT/.gemini/skills" + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' # No Color + +echo "🤖 Prowler AI Skills Setup" +echo "==========================" +echo "" + +# Count skills (directories with SKILL.md) +SKILL_COUNT=$(find "$SKILLS_SOURCE" -maxdepth 2 -name "SKILL.md" | wc -l | tr -d ' ') + +if [ "$SKILL_COUNT" -eq 0 ]; then + echo -e "${RED}No skills found in $SKILLS_SOURCE${NC}" + exit 1 +fi + +echo -e "${BLUE}Found $SKILL_COUNT skills to configure${NC}" +echo "" + +# ============================================================================= +# CLAUDE CODE SETUP (.claude/skills symlink - auto-discovery) +# ============================================================================= +echo -e "${YELLOW}[1/3] Setting up Claude Code...${NC}" + +if [ ! -d "$REPO_ROOT/.claude" ]; then + mkdir -p "$REPO_ROOT/.claude" +fi + +if [ -L "$CLAUDE_SKILLS_TARGET" ]; then + rm "$CLAUDE_SKILLS_TARGET" +elif [ -d "$CLAUDE_SKILLS_TARGET" ]; then + mv "$CLAUDE_SKILLS_TARGET" "$REPO_ROOT/.claude/skills.backup.$(date +%s)" +fi + +ln -s "$SKILLS_SOURCE" "$CLAUDE_SKILLS_TARGET" +echo -e "${GREEN} ✓ .claude/skills -> skills/${NC}" + +# ============================================================================= +# CODEX (OPENAI) SETUP (.codex/skills symlink) +# ============================================================================= +echo -e "${YELLOW}[2/3] Setting up Codex (OpenAI)...${NC}" + +if [ ! -d "$REPO_ROOT/.codex" ]; then + mkdir -p "$REPO_ROOT/.codex" +fi + +if [ -L "$CODEX_SKILLS_TARGET" ]; then + rm "$CODEX_SKILLS_TARGET" +elif [ -d "$CODEX_SKILLS_TARGET" ]; then + mv "$CODEX_SKILLS_TARGET" "$REPO_ROOT/.codex/skills.backup.$(date +%s)" +fi + +ln -s "$SKILLS_SOURCE" "$CODEX_SKILLS_TARGET" +echo -e "${GREEN} ✓ .codex/skills -> skills/${NC}" + +# ============================================================================= +# GEMINI CLI SETUP (.gemini/skills symlink - auto-discovery) +# ============================================================================= +echo -e "${YELLOW}[3/3] Setting up Gemini CLI...${NC}" + +if [ ! -d "$REPO_ROOT/.gemini" ]; then + mkdir -p "$REPO_ROOT/.gemini" +fi + +if [ -L "$GEMINI_SKILLS_TARGET" ]; then + rm "$GEMINI_SKILLS_TARGET" +elif [ -d "$GEMINI_SKILLS_TARGET" ]; then + mv "$GEMINI_SKILLS_TARGET" "$REPO_ROOT/.gemini/skills.backup.$(date +%s)" +fi + +ln -s "$SKILLS_SOURCE" "$GEMINI_SKILLS_TARGET" +echo -e "${GREEN} ✓ .gemini/skills -> skills/${NC}" + +# ============================================================================= +# SUMMARY +# ============================================================================= +echo "" +echo -e "${GREEN}✅ Successfully configured $SKILL_COUNT AI skills!${NC}" +echo "" +echo "Configuration created:" +echo " • Claude Code: .claude/skills/ (symlink, auto-discovery)" +echo " • Codex (OpenAI): .codex/skills/ (symlink, reads AGENTS.md)" +echo " • Gemini CLI: .gemini/skills/ (symlink, auto-discovery)" +echo " • GitHub Copilot: reads AGENTS.md from repo root (no setup needed)" +echo "" +echo "Available skills:" +echo " Generic: typescript, react-19, nextjs-15, playwright, pytest," +echo " django-drf, zod-4, zustand-5, tailwind-4, ai-sdk-5" +echo "" +echo " Prowler: prowler, prowler-api, prowler-ui, prowler-mcp," +echo " prowler-sdk-check, prowler-test-ui, prowler-test-api," +echo " prowler-test-sdk, prowler-compliance, prowler-docs," +echo " prowler-provider, prowler-pr" +echo "" +echo -e "${BLUE}Note: Restart your AI coding assistant to load the skills.${NC}" +echo -e "${BLUE} Claude/Gemini auto-discover skills from SKILL.md descriptions.${NC}" +echo -e "${BLUE} Codex/Copilot use AGENTS.md instructions to reference skills.${NC}" diff --git a/skills/skill-creator/SKILL.md b/skills/skill-creator/SKILL.md new file mode 100644 index 0000000000..d84aa5101a --- /dev/null +++ b/skills/skill-creator/SKILL.md @@ -0,0 +1,169 @@ +--- +name: skill-creator +description: > + Creates new AI agent skills following the Agent Skills spec. + Trigger: When user asks to create a new skill, add agent instructions, or document patterns for AI. +license: Apache-2.0 +metadata: + author: prowler-cloud + version: "1.0" +allowed-tools: Read, Edit, Write, Glob, Grep, Bash, WebFetch, WebSearch, Task +--- + +## When to Create a Skill + +Create a skill when: +- A pattern is used repeatedly and AI needs guidance +- Project-specific conventions differ from generic best practices +- Complex workflows need step-by-step instructions +- Decision trees help AI choose the right approach + +**Don't create a skill when:** +- Documentation already exists (create a reference instead) +- Pattern is trivial or self-explanatory +- It's a one-off task + +--- + +## Skill Structure + +``` +skills/{skill-name}/ +├── SKILL.md # Required - main skill file +├── assets/ # Optional - templates, schemas, examples +│ ├── template.py +│ └── schema.json +└── references/ # Optional - links to local docs + └── docs.md # Points to docs/developer-guide/*.mdx +``` + +--- + +## SKILL.md Template + +```markdown +--- +name: {skill-name} +description: > + {One-line description of what this skill does}. + Trigger: {When the AI should load this skill}. +license: Apache-2.0 +metadata: + author: prowler-cloud + version: "1.0" +--- + +## When to Use + +{Bullet points of when to use this skill} + +## Critical Patterns + +{The most important rules - what AI MUST know} + +## Code Examples + +{Minimal, focused examples} + +## Commands + +```bash +{Common commands} +``` + +## Resources + +- **Templates**: See [assets/](assets/) for {description} +- **Documentation**: See [references/](references/) for local docs +``` + +--- + +## Naming Conventions + +| Type | Pattern | Examples | +|------|---------|----------| +| Generic skill | `{technology}` | `pytest`, `playwright`, `typescript` | +| Prowler-specific | `prowler-{component}` | `prowler-api`, `prowler-ui`, `prowler-sdk-check` | +| Testing skill | `prowler-test-{component}` | `prowler-test-sdk`, `prowler-test-api` | +| Workflow skill | `{action}-{target}` | `skill-creator`, `jira-task` | + +--- + +## Decision: assets/ vs references/ + +``` +Need code templates? → assets/ +Need JSON schemas? → assets/ +Need example configs? → assets/ +Link to existing docs? → references/ +Link to external guides? → references/ (with local path) +``` + +**Key Rule**: `references/` should point to LOCAL files (`docs/developer-guide/*.mdx`), not web URLs. + +--- + +## Decision: Prowler-Specific vs Generic + +``` +Patterns apply to ANY project? → Generic skill (e.g., pytest, typescript) +Patterns are Prowler-specific? → prowler-{name} skill +Generic skill needs Prowler info? → Add references/ pointing to Prowler docs +``` + +--- + +## Frontmatter Fields + +| Field | Required | Description | +|-------|----------|-------------| +| `name` | Yes | Skill identifier (lowercase, hyphens) | +| `description` | Yes | What + Trigger in one block | +| `license` | Yes | Always `Apache-2.0` for Prowler | +| `metadata.author` | Yes | `prowler-cloud` | +| `metadata.version` | Yes | Semantic version as string | + +--- + +## Content Guidelines + +### DO +- Start with the most critical patterns +- Use tables for decision trees +- Keep code examples minimal and focused +- Include Commands section with copy-paste commands + +### DON'T +- Add Keywords section (agent searches frontmatter, not body) +- Duplicate content from existing docs (reference instead) +- Include lengthy explanations (link to docs) +- Add troubleshooting sections (keep focused) +- Use web URLs in references (use local paths) + +--- + +## Registering the Skill + +After creating the skill, add it to `AGENTS.md`: + +```markdown +| `{skill-name}` | {Description} | [SKILL.md](skills/{skill-name}/SKILL.md) | +``` + +--- + +## Checklist Before Creating + +- [ ] Skill doesn't already exist (check `skills/`) +- [ ] Pattern is reusable (not one-off) +- [ ] Name follows conventions +- [ ] Frontmatter is complete (description includes trigger keywords) +- [ ] Critical patterns are clear +- [ ] Code examples are minimal +- [ ] Commands section exists +- [ ] Added to AGENTS.md + +## Resources + +- **Templates**: See [assets/](assets/) for SKILL.md template diff --git a/skills/skill-creator/assets/SKILL-TEMPLATE.md b/skills/skill-creator/assets/SKILL-TEMPLATE.md new file mode 100644 index 0000000000..7639240245 --- /dev/null +++ b/skills/skill-creator/assets/SKILL-TEMPLATE.md @@ -0,0 +1,78 @@ +--- +name: {skill-name} +description: > + {Brief description of what this skill enables}. + Trigger: {When the AI should load this skill - be specific}. +license: Apache-2.0 +metadata: + author: prowler-cloud + version: "1.0" +--- + +## When to Use + +Use this skill when: +- {Condition 1} +- {Condition 2} +- {Condition 3} + +--- + +## Critical Patterns + +{The MOST important rules - what AI MUST follow} + +### Pattern 1: {Name} + +```{language} +{code example} +``` + +### Pattern 2: {Name} + +```{language} +{code example} +``` + +--- + +## Decision Tree + +``` +{Question 1}? → {Action A} +{Question 2}? → {Action B} +Otherwise → {Default action} +``` + +--- + +## Code Examples + +### Example 1: {Description} + +```{language} +{minimal, focused example} +``` + +### Example 2: {Description} + +```{language} +{minimal, focused example} +``` + +--- + +## Commands + +```bash +{command 1} # {description} +{command 2} # {description} +{command 3} # {description} +``` + +--- + +## Resources + +- **Templates**: See [assets/](assets/) for {description of templates} +- **Documentation**: See [references/](references/) for local developer guide links diff --git a/skills/tailwind-4/SKILL.md b/skills/tailwind-4/SKILL.md new file mode 100644 index 0000000000..84b50261ce --- /dev/null +++ b/skills/tailwind-4/SKILL.md @@ -0,0 +1,197 @@ +--- +name: tailwind-4 +description: > + Tailwind CSS 4 patterns and best practices. + Trigger: When styling with Tailwind - cn(), theme variables, no var() in className. +license: Apache-2.0 +metadata: + author: prowler-cloud + version: "1.0" +allowed-tools: Read, Edit, Write, Glob, Grep, Bash, WebFetch, WebSearch, Task +--- + +## Styling Decision Tree + +``` +Tailwind class exists? → className="..." +Dynamic value? → style={{ width: `${x}%` }} +Conditional styles? → cn("base", condition && "variant") +Static only? → className="..." (no cn() needed) +Library can't use class?→ style prop with var() constants +``` + +## Critical Rules + +### Never Use var() in className + +```typescript +// ❌ NEVER: var() in className +
+
+ +// ✅ ALWAYS: Use Tailwind semantic classes +
+
+``` + +### Never Use Hex Colors + +```typescript +// ❌ NEVER: Hex colors in className +

+

+ +// ✅ ALWAYS: Use Tailwind color classes +

+

+``` + +## The cn() Utility + +```typescript +import { clsx } from "clsx"; +import { twMerge } from "tailwind-merge"; + +export function cn(...inputs: ClassValue[]) { + return twMerge(clsx(inputs)); +} +``` + +### When to Use cn() + +```typescript +// ✅ Conditional classes +
+ +// ✅ Merging with potential conflicts + + +
+ ); +} +``` + +## Persist Middleware + +```typescript +import { create } from "zustand"; +import { persist } from "zustand/middleware"; + +interface SettingsStore { + theme: "light" | "dark"; + language: string; + setTheme: (theme: "light" | "dark") => void; + setLanguage: (language: string) => void; +} + +const useSettingsStore = create()( + persist( + (set) => ({ + theme: "light", + language: "en", + setTheme: (theme) => set({ theme }), + setLanguage: (language) => set({ language }), + }), + { + name: "settings-storage", // localStorage key + } + ) +); +``` + +## Selectors (Zustand 5) + +```typescript +// ✅ Select specific fields to prevent unnecessary re-renders +function UserName() { + const name = useUserStore((state) => state.name); + return {name}; +} + +// ✅ For multiple fields, use useShallow +import { useShallow } from "zustand/react/shallow"; + +function UserInfo() { + const { name, email } = useUserStore( + useShallow((state) => ({ name: state.name, email: state.email })) + ); + return
{name} - {email}
; +} + +// ❌ AVOID: Selecting entire store (causes re-render on any change) +const store = useUserStore(); // Re-renders on ANY state change +``` + +## Async Actions + +```typescript +interface UserStore { + user: User | null; + loading: boolean; + error: string | null; + fetchUser: (id: string) => Promise; +} + +const useUserStore = create((set) => ({ + user: null, + loading: false, + error: null, + + fetchUser: async (id) => { + set({ loading: true, error: null }); + try { + const response = await fetch(`/api/users/${id}`); + const user = await response.json(); + set({ user, loading: false }); + } catch (error) { + set({ error: "Failed to fetch user", loading: false }); + } + }, +})); +``` + +## Slices Pattern + +```typescript +// userSlice.ts +interface UserSlice { + user: User | null; + setUser: (user: User) => void; + clearUser: () => void; +} + +const createUserSlice = (set): UserSlice => ({ + user: null, + setUser: (user) => set({ user }), + clearUser: () => set({ user: null }), +}); + +// cartSlice.ts +interface CartSlice { + items: CartItem[]; + addItem: (item: CartItem) => void; + removeItem: (id: string) => void; +} + +const createCartSlice = (set): CartSlice => ({ + items: [], + addItem: (item) => set((state) => ({ items: [...state.items, item] })), + removeItem: (id) => set((state) => ({ + items: state.items.filter(i => i.id !== id) + })), +}); + +// store.ts +type Store = UserSlice & CartSlice; + +const useStore = create()((...args) => ({ + ...createUserSlice(...args), + ...createCartSlice(...args), +})); +``` + +## Immer Middleware + +```typescript +import { create } from "zustand"; +import { immer } from "zustand/middleware/immer"; + +interface TodoStore { + todos: Todo[]; + addTodo: (text: string) => void; + toggleTodo: (id: string) => void; +} + +const useTodoStore = create()( + immer((set) => ({ + todos: [], + + addTodo: (text) => set((state) => { + // Mutate directly with Immer! + state.todos.push({ id: crypto.randomUUID(), text, done: false }); + }), + + toggleTodo: (id) => set((state) => { + const todo = state.todos.find(t => t.id === id); + if (todo) todo.done = !todo.done; + }), + })) +); +``` + +## DevTools + +```typescript +import { create } from "zustand"; +import { devtools } from "zustand/middleware"; + +const useStore = create()( + devtools( + (set) => ({ + // store definition + }), + { name: "MyStore" } // Name in Redux DevTools + ) +); +``` + +## Outside React + +```typescript +// Access store outside components +const { count, increment } = useCounterStore.getState(); +increment(); + +// Subscribe to changes +const unsubscribe = useCounterStore.subscribe( + (state) => console.log("Count changed:", state.count) +); +``` diff --git a/tests/providers/aws/services/ec2/lib/security_groups_test.py b/tests/providers/aws/services/ec2/lib/security_groups_test.py index 9296a29c20..653bf420bd 100644 --- a/tests/providers/aws/services/ec2/lib/security_groups_test.py +++ b/tests/providers/aws/services/ec2/lib/security_groups_test.py @@ -48,7 +48,7 @@ class Test_is_cidr_public: with pytest.raises(ValueError) as ex: _is_cidr_public(cidr) - assert ex.type == ValueError + assert ex.type is ValueError assert ex.match(f"{cidr} has host bits set") def test__is_cidr_public_Public_IPv6_all_IPs_any_address_false(self): @@ -77,7 +77,7 @@ class Test_is_cidr_public: class Test_check_security_group: - def generate_ip_ranges_list(self, input_ip_ranges: [str], v4=True): + def generate_ip_ranges_list(self, input_ip_ranges: list[str], v4=True): cidr_ranges = "CidrIp" if v4 else "CidrIpv6" return [{cidr_ranges: ip, "Description": ""} for ip in input_ip_ranges] @@ -86,8 +86,8 @@ class Test_check_security_group: from_port: int, to_port: int, ip_protocol: str, - input_ipv4_ranges: [str], - input_ipv6_ranges: [str], + input_ipv4_ranges: list[str], + input_ipv6_ranges: list[str], ): """ ingress_rule_generator returns the following AWS Security Group IpPermissions Ingress Rule based on the input arguments diff --git a/tests/providers/aws/services/s3/s3_bucket_server_access_logging_enabled/s3_bucket_server_access_logging_enabled_test.py b/tests/providers/aws/services/s3/s3_bucket_server_access_logging_enabled/s3_bucket_server_access_logging_enabled_test.py index 5ab05e4959..849f2456b1 100644 --- a/tests/providers/aws/services/s3/s3_bucket_server_access_logging_enabled/s3_bucket_server_access_logging_enabled_test.py +++ b/tests/providers/aws/services/s3/s3_bucket_server_access_logging_enabled/s3_bucket_server_access_logging_enabled_test.py @@ -249,3 +249,42 @@ class Test_s3_bucket_server_access_logging_enabled: by_id[bucket_pass].resource_arn == f"arn:{aws_provider.identity.partition}:s3:::{bucket_pass}" ) + + @mock_aws + def test_bucket_logging_config_missing_loggingenabled_key(self): + s3_client_us_east_1 = client("s3", region_name=AWS_REGION_US_EAST_1) + bucket_name = "bucket_test_logging_empty" + s3_client_us_east_1.create_bucket(Bucket=bucket_name) + + # Explicitly set empty logging status (no LoggingEnabled) + s3_client_us_east_1.put_bucket_logging( + Bucket=bucket_name, + BucketLoggingStatus={}, + ) + + from prowler.providers.aws.services.s3.s3_service import S3 + + aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1]) + + with mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ): + with mock.patch( + "prowler.providers.aws.services.s3.s3_bucket_server_access_logging_enabled.s3_bucket_server_access_logging_enabled.s3_client", + new=S3(aws_provider), + ): + from prowler.providers.aws.services.s3.s3_bucket_server_access_logging_enabled.s3_bucket_server_access_logging_enabled import ( + s3_bucket_server_access_logging_enabled, + ) + + check = s3_bucket_server_access_logging_enabled() + result = check.execute() + + assert len(result) == 1 + assert result[0].resource_id == bucket_name + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == f"S3 Bucket {bucket_name} has server access logging disabled." + ) diff --git a/tests/providers/gcp/gcp_fixtures.py b/tests/providers/gcp/gcp_fixtures.py index c6182bbf0c..2f968f5555 100644 --- a/tests/providers/gcp/gcp_fixtures.py +++ b/tests/providers/gcp/gcp_fixtures.py @@ -32,6 +32,11 @@ def set_mocked_gcp_provider( provider.identity = GCPIdentityInfo( profile=profile, ) + provider.audit_config = { + "mig_min_zones": 2, + "max_unused_account_days": 30, + } + provider.fixer_config = {} return provider @@ -58,6 +63,7 @@ def mock_api_client(GCPService, service, api_version, _): mock_api_services_calls(client) mock_api_access_policies_calls(client) mock_api_instance_group_managers_calls(client) + mock_api_images_calls(client) return client @@ -1102,6 +1108,34 @@ def mock_api_sink_calls(client: MagicMock): } client.sinks().list_next.return_value = None + client.entries().list().execute.return_value = { + "entries": [ + { + "insertId": "audit-log-entry-1", + "timestamp": "2024-01-15T10:30:00Z", + "receiveTimestamp": "2024-01-15T10:30:01Z", + "resource": { + "type": "gce_instance", + "labels": { + "instance_id": "test-instance-1", + "project_id": GCP_PROJECT_ID, + }, + }, + "protoPayload": { + "serviceName": "compute.googleapis.com", + "methodName": "v1.compute.instances.insert", + "resourceName": "projects/test-project/zones/us-central1-a/instances/test-instance-1", + "authenticationInfo": { + "principalEmail": "user@example.com", + }, + "requestMetadata": { + "callerIp": "192.168.1.1", + }, + }, + }, + ] + } + def mock_api_services_calls(client: MagicMock): client.services().list().execute.return_value = { @@ -1260,3 +1294,53 @@ def mock_api_instance_group_managers_calls(client: MagicMock): ] } client.instanceGroupManagers().list_next.return_value = None + + +def mock_api_images_calls(client: MagicMock): + image1_id = str(uuid4()) + image2_id = str(uuid4()) + image3_id = str(uuid4()) + + client.images().list().execute.return_value = { + "items": [ + { + "name": "test-image-1", + "id": image1_id, + }, + { + "name": "test-image-2", + "id": image2_id, + }, + { + "name": "test-image-3", + "id": image3_id, + }, + ] + } + client.images().list_next.return_value = None + + def mock_get_image_iam_policy(project, resource): + return_value = MagicMock() + if resource == "test-image-1": + return_value.execute.return_value = { + "bindings": [ + { + "role": "roles/compute.imageUser", + "members": ["user:test@example.com"], + } + ] + } + elif resource == "test-image-2": + return_value.execute.return_value = { + "bindings": [ + { + "role": "roles/compute.imageUser", + "members": ["allAuthenticatedUsers"], + } + ] + } + elif resource == "test-image-3": + return_value.execute.side_effect = Exception("Permission denied") + return return_value + + client.images().getIamPolicy = mock_get_image_iam_policy diff --git a/tests/providers/gcp/services/compute/compute_image_not_publicly_shared/compute_image_not_publicly_shared_test.py b/tests/providers/gcp/services/compute/compute_image_not_publicly_shared/compute_image_not_publicly_shared_test.py new file mode 100644 index 0000000000..55bbaa837e --- /dev/null +++ b/tests/providers/gcp/services/compute/compute_image_not_publicly_shared/compute_image_not_publicly_shared_test.py @@ -0,0 +1,168 @@ +from unittest import mock + +from tests.providers.gcp.gcp_fixtures import GCP_PROJECT_ID, set_mocked_gcp_provider + + +class Test_compute_image_not_publicly_shared: + def test_compute_no_images(self): + compute_client = mock.MagicMock() + compute_client.images = [] + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_gcp_provider(), + ), + mock.patch( + "prowler.providers.gcp.services.compute.compute_image_not_publicly_shared.compute_image_not_publicly_shared.compute_client", + new=compute_client, + ), + ): + from prowler.providers.gcp.services.compute.compute_image_not_publicly_shared.compute_image_not_publicly_shared import ( + compute_image_not_publicly_shared, + ) + + check = compute_image_not_publicly_shared() + result = check.execute() + assert len(result) == 0 + + def test_image_not_publicly_shared(self): + compute_client = mock.MagicMock() + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_gcp_provider(), + ), + mock.patch( + "prowler.providers.gcp.services.compute.compute_image_not_publicly_shared.compute_image_not_publicly_shared.compute_client", + new=compute_client, + ), + ): + from prowler.providers.gcp.services.compute.compute_image_not_publicly_shared.compute_image_not_publicly_shared import ( + compute_image_not_publicly_shared, + ) + from prowler.providers.gcp.services.compute.compute_service import Image + + image = Image( + name="private-image", + id="1234567890", + project_id=GCP_PROJECT_ID, + publicly_shared=False, + ) + + compute_client.project_ids = [GCP_PROJECT_ID] + compute_client.images = [image] + + check = compute_image_not_publicly_shared() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "PASS" + assert ( + result[0].status_extended + == "Compute Engine disk image private-image is not publicly shared." + ) + assert result[0].resource_id == "1234567890" + assert result[0].project_id == GCP_PROJECT_ID + assert result[0].resource_name == "private-image" + assert result[0].location == "global" + + def test_image_publicly_shared_with_all_authenticated_users(self): + from prowler.providers.gcp.services.compute.compute_service import Image + + image = Image( + name="public-image", + id="1234567890", + project_id=GCP_PROJECT_ID, + publicly_shared=True, + ) + + compute_client = mock.MagicMock() + compute_client.project_ids = [GCP_PROJECT_ID] + compute_client.images = [image] + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_gcp_provider(), + ), + mock.patch( + "prowler.providers.gcp.services.compute.compute_image_not_publicly_shared.compute_image_not_publicly_shared.compute_client", + new=compute_client, + ), + ): + from prowler.providers.gcp.services.compute.compute_image_not_publicly_shared.compute_image_not_publicly_shared import ( + compute_image_not_publicly_shared, + ) + + check = compute_image_not_publicly_shared() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == "Compute Engine disk image public-image is publicly shared with allAuthenticatedUsers." + ) + assert result[0].resource_id == "1234567890" + assert result[0].project_id == GCP_PROJECT_ID + assert result[0].resource_name == "public-image" + assert result[0].location == "global" + + def test_multiple_images_mixed_sharing(self): + from prowler.providers.gcp.services.compute.compute_service import Image + + private_image = Image( + name="private-image", + id="1111111111", + project_id=GCP_PROJECT_ID, + publicly_shared=False, + ) + + public_image = Image( + name="public-image", + id="2222222222", + project_id=GCP_PROJECT_ID, + publicly_shared=True, + ) + + compute_client = mock.MagicMock() + compute_client.project_ids = [GCP_PROJECT_ID] + compute_client.images = [private_image, public_image] + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_gcp_provider(), + ), + mock.patch( + "prowler.providers.gcp.services.compute.compute_image_not_publicly_shared.compute_image_not_publicly_shared.compute_client", + new=compute_client, + ), + ): + from prowler.providers.gcp.services.compute.compute_image_not_publicly_shared.compute_image_not_publicly_shared import ( + compute_image_not_publicly_shared, + ) + + check = compute_image_not_publicly_shared() + result = check.execute() + + assert len(result) == 2 + + private_result = next( + r for r in result if r.resource_name == "private-image" + ) + public_result = next(r for r in result if r.resource_name == "public-image") + + assert private_result.status == "PASS" + assert ( + private_result.status_extended + == "Compute Engine disk image private-image is not publicly shared." + ) + + assert public_result.status == "FAIL" + assert ( + public_result.status_extended + == "Compute Engine disk image public-image is publicly shared with allAuthenticatedUsers." + ) diff --git a/tests/providers/gcp/services/compute/compute_instance_single_network_interface/compute_instance_single_network_interface_test.py b/tests/providers/gcp/services/compute/compute_instance_single_network_interface/compute_instance_single_network_interface_test.py new file mode 100644 index 0000000000..413ff7df36 --- /dev/null +++ b/tests/providers/gcp/services/compute/compute_instance_single_network_interface/compute_instance_single_network_interface_test.py @@ -0,0 +1,357 @@ +from unittest import mock + +from tests.providers.gcp.gcp_fixtures import GCP_PROJECT_ID, set_mocked_gcp_provider + + +class Test_compute_instance_single_network_interface: + def test_compute_no_instances(self): + compute_client = mock.MagicMock() + compute_client.instances = [] + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_gcp_provider(), + ), + mock.patch( + "prowler.providers.gcp.services.compute.compute_instance_single_network_interface.compute_instance_single_network_interface.compute_client", + new=compute_client, + ), + ): + from prowler.providers.gcp.services.compute.compute_instance_single_network_interface.compute_instance_single_network_interface import ( + compute_instance_single_network_interface, + ) + + check = compute_instance_single_network_interface() + result = check.execute() + assert len(result) == 0 + + def test_single_network_interface(self): + compute_client = mock.MagicMock() + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_gcp_provider(), + ), + mock.patch( + "prowler.providers.gcp.services.compute.compute_instance_single_network_interface.compute_instance_single_network_interface.compute_client", + new=compute_client, + ), + ): + from prowler.providers.gcp.services.compute.compute_instance_single_network_interface.compute_instance_single_network_interface import ( + compute_instance_single_network_interface, + ) + from prowler.providers.gcp.services.compute.compute_service import ( + Instance, + NetworkInterface, + ) + + instance = Instance( + name="test-instance", + id="1234567890", + zone="us-central1-a", + region="us-central1", + public_ip=False, + metadata={}, + shielded_enabled_vtpm=True, + shielded_enabled_integrity_monitoring=True, + confidential_computing=False, + service_accounts=[ + {"email": "123-compute@developer.gserviceaccount.com"} + ], + ip_forward=False, + disks_encryption=[], + project_id=GCP_PROJECT_ID, + network_interfaces=[ + NetworkInterface( + name="nic0", network="default", subnetwork="default" + ) + ], + ) + + compute_client.project_ids = [GCP_PROJECT_ID] + compute_client.instances = [instance] + + check = compute_instance_single_network_interface() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "PASS" + assert ( + result[0].status_extended + == "VM Instance test-instance has a single network interface: nic0." + ) + assert result[0].resource_id == "1234567890" + assert result[0].project_id == GCP_PROJECT_ID + assert result[0].resource_name == "test-instance" + assert result[0].location == "us-central1" + + def test_multiple_network_interfaces(self): + from prowler.providers.gcp.services.compute.compute_service import ( + Instance, + NetworkInterface, + ) + + instance = Instance( + name="multi-nic-instance", + id="9876543210", + zone="us-central1-a", + region="us-central1", + public_ip=True, + metadata={}, + shielded_enabled_vtpm=True, + shielded_enabled_integrity_monitoring=True, + confidential_computing=False, + service_accounts=[ + {"email": f"{GCP_PROJECT_ID}-compute@developer.gserviceaccount.com"} + ], + ip_forward=False, + disks_encryption=[], + project_id=GCP_PROJECT_ID, + network_interfaces=[ + NetworkInterface(name="nic0", network="default", subnetwork="subnet-1"), + NetworkInterface(name="nic1", network="vpc-2", subnetwork="subnet-2"), + NetworkInterface(name="nic2", network="vpc-3", subnetwork="subnet-3"), + ], + ) + + compute_client = mock.MagicMock() + compute_client.project_ids = [GCP_PROJECT_ID] + compute_client.instances = [instance] + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_gcp_provider(), + ), + mock.patch( + "prowler.providers.gcp.services.compute.compute_instance_single_network_interface.compute_instance_single_network_interface.compute_client", + new=compute_client, + ), + ): + from prowler.providers.gcp.services.compute.compute_instance_single_network_interface.compute_instance_single_network_interface import ( + compute_instance_single_network_interface, + ) + + check = compute_instance_single_network_interface() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == "VM Instance multi-nic-instance has 3 network interfaces: nic0, nic1, nic2." + ) + assert result[0].resource_id == "9876543210" + assert result[0].project_id == GCP_PROJECT_ID + assert result[0].resource_name == "multi-nic-instance" + assert result[0].location == "us-central1" + + def test_two_network_interfaces(self): + from prowler.providers.gcp.services.compute.compute_service import ( + Instance, + NetworkInterface, + ) + + instance = Instance( + name="dual-nic-instance", + id="1111111111", + zone="europe-west1-b", + region="europe-west1", + public_ip=False, + metadata={}, + shielded_enabled_vtpm=True, + shielded_enabled_integrity_monitoring=True, + confidential_computing=False, + service_accounts=[], + ip_forward=False, + disks_encryption=[], + project_id=GCP_PROJECT_ID, + network_interfaces=[ + NetworkInterface(name="nic0", network="default", subnetwork="default"), + NetworkInterface(name="nic1", network="vpc-2", subnetwork="subnet-2"), + ], + ) + + compute_client = mock.MagicMock() + compute_client.project_ids = [GCP_PROJECT_ID] + compute_client.instances = [instance] + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_gcp_provider(), + ), + mock.patch( + "prowler.providers.gcp.services.compute.compute_instance_single_network_interface.compute_instance_single_network_interface.compute_client", + new=compute_client, + ), + ): + from prowler.providers.gcp.services.compute.compute_instance_single_network_interface.compute_instance_single_network_interface import ( + compute_instance_single_network_interface, + ) + + check = compute_instance_single_network_interface() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == "VM Instance dual-nic-instance has 2 network interfaces: nic0, nic1." + ) + assert result[0].resource_id == "1111111111" + assert result[0].project_id == GCP_PROJECT_ID + assert result[0].resource_name == "dual-nic-instance" + assert result[0].location == "europe-west1" + + def test_mixed_instances(self): + from prowler.providers.gcp.services.compute.compute_service import ( + Instance, + NetworkInterface, + ) + + instance_single_nic = Instance( + name="single-nic-instance", + id="1111111111", + zone="us-central1-a", + region="us-central1", + public_ip=False, + metadata={}, + shielded_enabled_vtpm=True, + shielded_enabled_integrity_monitoring=True, + confidential_computing=False, + service_accounts=[], + ip_forward=False, + disks_encryption=[], + project_id=GCP_PROJECT_ID, + network_interfaces=[ + NetworkInterface(name="nic0", network="default", subnetwork="default") + ], + ) + + instance_multi_nic = Instance( + name="multi-nic-instance", + id="2222222222", + zone="us-central1-a", + region="us-central1", + public_ip=True, + metadata={}, + shielded_enabled_vtpm=True, + shielded_enabled_integrity_monitoring=True, + confidential_computing=False, + service_accounts=[], + ip_forward=False, + disks_encryption=[], + project_id=GCP_PROJECT_ID, + network_interfaces=[ + NetworkInterface(name="nic0", network="default", subnetwork="default"), + NetworkInterface(name="nic1", network="vpc-2", subnetwork="subnet-2"), + NetworkInterface(name="nic2", network="vpc-3", subnetwork="subnet-3"), + NetworkInterface(name="nic3", network="vpc-4", subnetwork="subnet-4"), + ], + ) + + compute_client = mock.MagicMock() + compute_client.project_ids = [GCP_PROJECT_ID] + compute_client.instances = [instance_single_nic, instance_multi_nic] + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_gcp_provider(), + ), + mock.patch( + "prowler.providers.gcp.services.compute.compute_instance_single_network_interface.compute_instance_single_network_interface.compute_client", + new=compute_client, + ), + ): + from prowler.providers.gcp.services.compute.compute_instance_single_network_interface.compute_instance_single_network_interface import ( + compute_instance_single_network_interface, + ) + + check = compute_instance_single_network_interface() + result = check.execute() + + assert len(result) == 2 + + # First instance: single NIC (PASS) + assert result[0].status == "PASS" + assert ( + result[0].status_extended + == "VM Instance single-nic-instance has a single network interface: nic0." + ) + assert result[0].resource_id == "1111111111" + assert result[0].resource_name == "single-nic-instance" + + # Second instance: multiple NICs (FAIL) + assert result[1].status == "FAIL" + assert ( + result[1].status_extended + == "VM Instance multi-nic-instance has 4 network interfaces: nic0, nic1, nic2, nic3." + ) + assert result[1].resource_id == "2222222222" + assert result[1].resource_name == "multi-nic-instance" + + def test_gke_instance_multiple_network_interfaces(self): + from prowler.providers.gcp.services.compute.compute_service import ( + Instance, + NetworkInterface, + ) + + instance = Instance( + name="gke-cluster-default-pool-12345678-abcd", + id="9999999999", + zone="us-central1-a", + region="us-central1", + public_ip=False, + metadata={}, + shielded_enabled_vtpm=True, + shielded_enabled_integrity_monitoring=True, + confidential_computing=False, + service_accounts=[], + ip_forward=False, + disks_encryption=[], + project_id=GCP_PROJECT_ID, + network_interfaces=[ + NetworkInterface( + name="nic0", network="gke-network", subnetwork="gke-subnet" + ), + NetworkInterface( + name="nic1", network="gke-network-2", subnetwork="gke-subnet-2" + ), + ], + ) + + compute_client = mock.MagicMock() + compute_client.project_ids = [GCP_PROJECT_ID] + compute_client.instances = [instance] + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_gcp_provider(), + ), + mock.patch( + "prowler.providers.gcp.services.compute.compute_instance_single_network_interface.compute_instance_single_network_interface.compute_client", + new=compute_client, + ), + ): + from prowler.providers.gcp.services.compute.compute_instance_single_network_interface.compute_instance_single_network_interface import ( + compute_instance_single_network_interface, + ) + + check = compute_instance_single_network_interface() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "MANUAL" + assert ( + result[0].status_extended + == "VM Instance gke-cluster-default-pool-12345678-abcd has 2 network interfaces: nic0, nic1. This is a GKE-managed instance which may legitimately require multiple interfaces. Manual review recommended." + ) + assert result[0].resource_id == "9999999999" + assert result[0].project_id == GCP_PROJECT_ID + assert result[0].resource_name == "gke-cluster-default-pool-12345678-abcd" + assert result[0].location == "us-central1" diff --git a/tests/providers/gcp/services/compute/compute_service_test.py b/tests/providers/gcp/services/compute/compute_service_test.py index 4f8c0c0390..28a3466a5d 100644 --- a/tests/providers/gcp/services/compute/compute_service_test.py +++ b/tests/providers/gcp/services/compute/compute_service_test.py @@ -60,6 +60,7 @@ class TestComputeService: assert not compute_client.instances[0].automatic_restart assert not compute_client.instances[0].preemptible assert compute_client.instances[0].provisioning_model == "STANDARD" + assert len(compute_client.instances[0].network_interfaces) == 1 assert compute_client.instances[1].name == "instance2" assert compute_client.instances[1].id.__class__.__name__ == "str" @@ -84,6 +85,7 @@ class TestComputeService: assert not compute_client.instances[1].automatic_restart assert not compute_client.instances[1].preemptible assert compute_client.instances[1].provisioning_model == "STANDARD" + assert len(compute_client.instances[1].network_interfaces) == 0 assert len(compute_client.networks) == 3 assert compute_client.networks[0].name == "network1" @@ -258,3 +260,20 @@ class TestComputeService: assert len(zonal_mig.auto_healing_policies) == 1 assert zonal_mig.auto_healing_policies[0].health_check == "tcp-health-check" assert zonal_mig.auto_healing_policies[0].initial_delay_sec == 120 + + # Test images + assert len(compute_client.images) == 3 + assert compute_client.images[0].name == "test-image-1" + assert compute_client.images[0].id.__class__.__name__ == "str" + assert compute_client.images[0].project_id == GCP_PROJECT_ID + assert not compute_client.images[0].publicly_shared + + assert compute_client.images[1].name == "test-image-2" + assert compute_client.images[1].id.__class__.__name__ == "str" + assert compute_client.images[1].project_id == GCP_PROJECT_ID + assert compute_client.images[1].publicly_shared + + assert compute_client.images[2].name == "test-image-3" + assert compute_client.images[2].id.__class__.__name__ == "str" + assert compute_client.images[2].project_id == GCP_PROJECT_ID + assert not compute_client.images[2].publicly_shared diff --git a/tests/providers/gcp/services/logging/logging_log_metric_filter_and_alert_for_compute_configuration_changes_enabled/logging_log_metric_filter_and_alert_for_compute_configuration_changes_enabled_test.py b/tests/providers/gcp/services/logging/logging_log_metric_filter_and_alert_for_compute_configuration_changes_enabled/logging_log_metric_filter_and_alert_for_compute_configuration_changes_enabled_test.py new file mode 100644 index 0000000000..563b4e49ac --- /dev/null +++ b/tests/providers/gcp/services/logging/logging_log_metric_filter_and_alert_for_compute_configuration_changes_enabled/logging_log_metric_filter_and_alert_for_compute_configuration_changes_enabled_test.py @@ -0,0 +1,348 @@ +from unittest.mock import MagicMock, patch + +from prowler.providers.gcp.models import GCPProject +from tests.providers.gcp.gcp_fixtures import ( + GCP_EU1_LOCATION, + GCP_PROJECT_ID, + set_mocked_gcp_provider, +) + + +class Test_logging_log_metric_filter_and_alert_for_compute_configuration_changes_enabled: + def test_no_projects(self): + logging_client = MagicMock() + monitoring_client = MagicMock() + + with ( + patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_gcp_provider(), + ), + patch( + "prowler.providers.gcp.services.logging.logging_log_metric_filter_and_alert_for_compute_configuration_changes_enabled.logging_log_metric_filter_and_alert_for_compute_configuration_changes_enabled.logging_client", + new=logging_client, + ), + patch( + "prowler.providers.gcp.services.logging.logging_log_metric_filter_and_alert_for_compute_configuration_changes_enabled.logging_log_metric_filter_and_alert_for_compute_configuration_changes_enabled.monitoring_client", + new=monitoring_client, + ), + ): + from prowler.providers.gcp.services.logging.logging_log_metric_filter_and_alert_for_compute_configuration_changes_enabled.logging_log_metric_filter_and_alert_for_compute_configuration_changes_enabled import ( + logging_log_metric_filter_and_alert_for_compute_configuration_changes_enabled, + ) + + logging_client.metrics = [] + logging_client.project_ids = [] + monitoring_client.alert_policies = [] + + check = ( + logging_log_metric_filter_and_alert_for_compute_configuration_changes_enabled() + ) + result = check.execute() + assert len(result) == 0 + + def test_no_log_metric_filters_no_alerts_one_project(self): + logging_client = MagicMock() + monitoring_client = MagicMock() + + with ( + patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_gcp_provider(), + ), + patch( + "prowler.providers.gcp.services.logging.logging_log_metric_filter_and_alert_for_compute_configuration_changes_enabled.logging_log_metric_filter_and_alert_for_compute_configuration_changes_enabled.logging_client", + new=logging_client, + ), + patch( + "prowler.providers.gcp.services.logging.logging_log_metric_filter_and_alert_for_compute_configuration_changes_enabled.logging_log_metric_filter_and_alert_for_compute_configuration_changes_enabled.monitoring_client", + new=monitoring_client, + ), + ): + from prowler.providers.gcp.services.logging.logging_log_metric_filter_and_alert_for_compute_configuration_changes_enabled.logging_log_metric_filter_and_alert_for_compute_configuration_changes_enabled import ( + logging_log_metric_filter_and_alert_for_compute_configuration_changes_enabled, + ) + + logging_client.metrics = [] + logging_client.project_ids = [GCP_PROJECT_ID] + logging_client.region = GCP_EU1_LOCATION + logging_client.projects = { + GCP_PROJECT_ID: GCPProject( + id=GCP_PROJECT_ID, + number="123456789012", + name="test", + labels={}, + lifecycle_state="ACTIVE", + ) + } + + monitoring_client.alert_policies = [] + + check = ( + logging_log_metric_filter_and_alert_for_compute_configuration_changes_enabled() + ) + result = check.execute() + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == f"There are no log metric filters or alerts associated for Compute Engine configuration changes in project {GCP_PROJECT_ID}." + ) + assert result[0].resource_id == GCP_PROJECT_ID + assert result[0].resource_name == "test" + assert result[0].project_id == GCP_PROJECT_ID + assert result[0].location == GCP_EU1_LOCATION + + def test_no_log_metric_filters_no_alerts_one_project_empty_name(self): + logging_client = MagicMock() + monitoring_client = MagicMock() + + with ( + patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_gcp_provider(), + ), + patch( + "prowler.providers.gcp.services.logging.logging_log_metric_filter_and_alert_for_compute_configuration_changes_enabled.logging_log_metric_filter_and_alert_for_compute_configuration_changes_enabled.logging_client", + new=logging_client, + ), + patch( + "prowler.providers.gcp.services.logging.logging_log_metric_filter_and_alert_for_compute_configuration_changes_enabled.logging_log_metric_filter_and_alert_for_compute_configuration_changes_enabled.monitoring_client", + new=monitoring_client, + ), + ): + from prowler.providers.gcp.services.logging.logging_log_metric_filter_and_alert_for_compute_configuration_changes_enabled.logging_log_metric_filter_and_alert_for_compute_configuration_changes_enabled import ( + logging_log_metric_filter_and_alert_for_compute_configuration_changes_enabled, + ) + + logging_client.metrics = [] + logging_client.project_ids = [GCP_PROJECT_ID] + logging_client.region = GCP_EU1_LOCATION + logging_client.projects = { + GCP_PROJECT_ID: GCPProject( + id=GCP_PROJECT_ID, + number="123456789012", + name="", + labels={}, + lifecycle_state="ACTIVE", + ) + } + + monitoring_client.alert_policies = [] + + check = ( + logging_log_metric_filter_and_alert_for_compute_configuration_changes_enabled() + ) + result = check.execute() + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == f"There are no log metric filters or alerts associated for Compute Engine configuration changes in project {GCP_PROJECT_ID}." + ) + assert result[0].resource_id == GCP_PROJECT_ID + assert result[0].resource_name == "GCP Project" + assert result[0].project_id == GCP_PROJECT_ID + assert result[0].location == GCP_EU1_LOCATION + + def test_log_metric_filters_no_alerts(self): + logging_client = MagicMock() + monitoring_client = MagicMock() + + with ( + patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_gcp_provider(), + ), + patch( + "prowler.providers.gcp.services.logging.logging_log_metric_filter_and_alert_for_compute_configuration_changes_enabled.logging_log_metric_filter_and_alert_for_compute_configuration_changes_enabled.logging_client", + new=logging_client, + ), + patch( + "prowler.providers.gcp.services.logging.logging_log_metric_filter_and_alert_for_compute_configuration_changes_enabled.logging_log_metric_filter_and_alert_for_compute_configuration_changes_enabled.monitoring_client", + new=monitoring_client, + ), + ): + from prowler.providers.gcp.services.logging.logging_log_metric_filter_and_alert_for_compute_configuration_changes_enabled.logging_log_metric_filter_and_alert_for_compute_configuration_changes_enabled import ( + logging_log_metric_filter_and_alert_for_compute_configuration_changes_enabled, + ) + from prowler.providers.gcp.services.logging.logging_service import Metric + + logging_client.metrics = [ + Metric( + name="compute_config_changes", + type="logging.googleapis.com/user/compute_config_changes", + filter='protoPayload.serviceName="compute.googleapis.com"', + project_id=GCP_PROJECT_ID, + ) + ] + logging_client.project_ids = [GCP_PROJECT_ID] + logging_client.region = GCP_EU1_LOCATION + + monitoring_client.alert_policies = [] + + check = ( + logging_log_metric_filter_and_alert_for_compute_configuration_changes_enabled() + ) + result = check.execute() + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == f"Log metric filter compute_config_changes found but no alerts associated in project {GCP_PROJECT_ID}." + ) + assert result[0].resource_id == "compute_config_changes" + assert result[0].resource_name == "compute_config_changes" + assert result[0].project_id == GCP_PROJECT_ID + assert result[0].location == GCP_EU1_LOCATION + + def test_log_metric_filters_with_alerts(self): + logging_client = MagicMock() + monitoring_client = MagicMock() + + with ( + patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_gcp_provider(), + ), + patch( + "prowler.providers.gcp.services.logging.logging_log_metric_filter_and_alert_for_compute_configuration_changes_enabled.logging_log_metric_filter_and_alert_for_compute_configuration_changes_enabled.logging_client", + new=logging_client, + ), + patch( + "prowler.providers.gcp.services.logging.logging_log_metric_filter_and_alert_for_compute_configuration_changes_enabled.logging_log_metric_filter_and_alert_for_compute_configuration_changes_enabled.monitoring_client", + new=monitoring_client, + ), + ): + from prowler.providers.gcp.services.logging.logging_log_metric_filter_and_alert_for_compute_configuration_changes_enabled.logging_log_metric_filter_and_alert_for_compute_configuration_changes_enabled import ( + logging_log_metric_filter_and_alert_for_compute_configuration_changes_enabled, + ) + from prowler.providers.gcp.services.logging.logging_service import Metric + from prowler.providers.gcp.services.monitoring.monitoring_service import ( + AlertPolicy, + ) + + logging_client.metrics = [ + Metric( + name="compute_config_changes", + type="logging.googleapis.com/user/compute_config_changes", + filter='protoPayload.serviceName="compute.googleapis.com"', + project_id=GCP_PROJECT_ID, + ) + ] + logging_client.project_ids = [GCP_PROJECT_ID] + logging_client.region = GCP_EU1_LOCATION + + monitoring_client.alert_policies = [ + AlertPolicy( + name=f"projects/{GCP_PROJECT_ID}/alertPolicies/12345", + display_name="Compute Config Alert", + enabled=True, + filters=[ + 'metric.type = "logging.googleapis.com/user/compute_config_changes"', + ], + project_id=GCP_PROJECT_ID, + ) + ] + + check = ( + logging_log_metric_filter_and_alert_for_compute_configuration_changes_enabled() + ) + result = check.execute() + assert len(result) == 1 + assert result[0].status == "PASS" + assert ( + result[0].status_extended + == f"Log metric filter compute_config_changes found with alert policy Compute Config Alert associated in project {GCP_PROJECT_ID}." + ) + assert result[0].resource_id == "compute_config_changes" + assert result[0].resource_name == "compute_config_changes" + assert result[0].project_id == GCP_PROJECT_ID + assert result[0].location == GCP_EU1_LOCATION + + def test_multiple_projects_mixed_results(self): + logging_client = MagicMock() + monitoring_client = MagicMock() + + project_id_1 = "project-with-monitoring" + project_id_2 = "project-without-monitoring" + + with ( + patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_gcp_provider( + project_ids=[project_id_1, project_id_2] + ), + ), + patch( + "prowler.providers.gcp.services.logging.logging_log_metric_filter_and_alert_for_compute_configuration_changes_enabled.logging_log_metric_filter_and_alert_for_compute_configuration_changes_enabled.logging_client", + new=logging_client, + ), + patch( + "prowler.providers.gcp.services.logging.logging_log_metric_filter_and_alert_for_compute_configuration_changes_enabled.logging_log_metric_filter_and_alert_for_compute_configuration_changes_enabled.monitoring_client", + new=monitoring_client, + ), + ): + from prowler.providers.gcp.services.logging.logging_log_metric_filter_and_alert_for_compute_configuration_changes_enabled.logging_log_metric_filter_and_alert_for_compute_configuration_changes_enabled import ( + logging_log_metric_filter_and_alert_for_compute_configuration_changes_enabled, + ) + from prowler.providers.gcp.services.logging.logging_service import Metric + from prowler.providers.gcp.services.monitoring.monitoring_service import ( + AlertPolicy, + ) + + logging_client.metrics = [ + Metric( + name="compute_config_changes", + type="logging.googleapis.com/user/compute_config_changes", + filter='protoPayload.serviceName="compute.googleapis.com"', + project_id=project_id_1, + ) + ] + logging_client.project_ids = [project_id_1, project_id_2] + logging_client.region = GCP_EU1_LOCATION + logging_client.projects = { + project_id_1: GCPProject( + id=project_id_1, + number="111111111111", + name="test-project-1", + labels={}, + lifecycle_state="ACTIVE", + ), + project_id_2: GCPProject( + id=project_id_2, + number="222222222222", + name="test-project-2", + labels={}, + lifecycle_state="ACTIVE", + ), + } + + monitoring_client.alert_policies = [ + AlertPolicy( + name=f"projects/{project_id_1}/alertPolicies/12345", + display_name="Compute Config Alert", + enabled=True, + filters=[ + 'metric.type = "logging.googleapis.com/user/compute_config_changes"', + ], + project_id=project_id_1, + ) + ] + + check = ( + logging_log_metric_filter_and_alert_for_compute_configuration_changes_enabled() + ) + result = check.execute() + assert len(result) == 2 + + # Project 1 should PASS (has metric + alert) + pass_result = [r for r in result if r.status == "PASS"][0] + assert pass_result.project_id == project_id_1 + assert "compute_config_changes" in pass_result.status_extended + assert "Compute Config Alert" in pass_result.status_extended + + # Project 2 should FAIL (no metric) + fail_result = [r for r in result if r.status == "FAIL"][0] + assert fail_result.project_id == project_id_2 + assert "no log metric filters" in fail_result.status_extended diff --git a/tests/providers/oraclecloud/oraclecloud_provider_test.py b/tests/providers/oraclecloud/oraclecloud_provider_test.py new file mode 100644 index 0000000000..dd3b7b7d27 --- /dev/null +++ b/tests/providers/oraclecloud/oraclecloud_provider_test.py @@ -0,0 +1,201 @@ +from unittest.mock import MagicMock, patch + +import pytest + +from prowler.providers.oraclecloud.exceptions.exceptions import ( + OCIAuthenticationError, + OCIInvalidConfigError, +) +from prowler.providers.oraclecloud.models import OCISession +from prowler.providers.oraclecloud.oraclecloud_provider import OraclecloudProvider + + +class TestSetIdentityAuthenticationErrors: + """Tests for authentication error handling in set_identity()""" + + @pytest.fixture + def mock_session(self): + """Create a mock OCI session.""" + session = OCISession( + config={ + "tenancy": "ocid1.tenancy.oc1..aaaaaaaexample", + "user": "ocid1.user.oc1..aaaaaaaexample", + "region": "us-ashburn-1", + "fingerprint": "aa:bb:cc:dd:ee:ff:00:11:22:33:44:55:66:77:88:99", + }, + signer=None, + profile="DEFAULT", + ) + return session + + def test_authentication_error_401_raises_exception(self, mock_session): + """Test 401 error raises OCIAuthenticationError.""" + with patch("oci.identity.IdentityClient") as mock_identity_client: + mock_client_instance = MagicMock() + mock_client_instance.get_tenancy.side_effect = self._create_service_error( + 401, "Authentication failed" + ) + mock_identity_client.return_value = mock_client_instance + + with pytest.raises(OCIAuthenticationError) as exc_info: + OraclecloudProvider.set_identity(mock_session) + + assert "OCI credential validation failed" in str(exc_info.value) + + def test_authentication_error_403_raises_exception(self, mock_session): + """Test 403 error raises OCIAuthenticationError.""" + with patch("oci.identity.IdentityClient") as mock_identity_client: + mock_client_instance = MagicMock() + mock_client_instance.get_tenancy.side_effect = self._create_service_error( + 403, "Forbidden access" + ) + mock_identity_client.return_value = mock_client_instance + + with pytest.raises(OCIAuthenticationError) as exc_info: + OraclecloudProvider.set_identity(mock_session) + + assert "OCI credential validation failed" in str(exc_info.value) + + def test_authentication_error_404_raises_exception(self, mock_session): + """Test 404 error raises OCIAuthenticationError.""" + with patch("oci.identity.IdentityClient") as mock_identity_client: + mock_client_instance = MagicMock() + mock_client_instance.get_tenancy.side_effect = self._create_service_error( + 404, "Resource not found" + ) + mock_identity_client.return_value = mock_client_instance + + with pytest.raises(OCIAuthenticationError) as exc_info: + OraclecloudProvider.set_identity(mock_session) + + assert "OCI credential validation failed" in str(exc_info.value) + + def test_service_error_500_raises_exception(self, mock_session): + """Test 500 error raises OCIAuthenticationError (can't validate credentials).""" + with patch("oci.identity.IdentityClient") as mock_identity_client: + mock_client_instance = MagicMock() + mock_client_instance.get_tenancy.side_effect = self._create_service_error( + 500, "Internal server error" + ) + mock_identity_client.return_value = mock_client_instance + + with pytest.raises(OCIAuthenticationError) as exc_info: + OraclecloudProvider.set_identity(mock_session) + + assert "OCI credential validation failed" in str(exc_info.value) + + def test_invalid_private_key_raises_exception(self, mock_session): + """Test InvalidPrivateKey exception raises OCIAuthenticationError.""" + with patch("oci.identity.IdentityClient") as mock_identity_client: + import oci + + mock_client_instance = MagicMock() + mock_client_instance.get_tenancy.side_effect = ( + oci.exceptions.InvalidPrivateKey("Invalid private key") + ) + mock_identity_client.return_value = mock_client_instance + + with pytest.raises(OCIAuthenticationError) as exc_info: + OraclecloudProvider.set_identity(mock_session) + + assert "Invalid OCI private key format" in str(exc_info.value) + + def test_generic_exception_raises_authentication_error(self, mock_session): + """Test generic exception raises OCIAuthenticationError.""" + with patch("oci.identity.IdentityClient") as mock_identity_client: + mock_client_instance = MagicMock() + mock_client_instance.get_tenancy.side_effect = Exception("Unexpected error") + mock_identity_client.return_value = mock_client_instance + + with pytest.raises(OCIAuthenticationError) as exc_info: + OraclecloudProvider.set_identity(mock_session) + + assert "Failed to authenticate with OCI" in str(exc_info.value) + + def test_successful_authentication(self, mock_session): + """Test successful authentication returns identity info.""" + with patch("oci.identity.IdentityClient") as mock_identity_client: + mock_tenancy = MagicMock() + mock_tenancy.name = "test-tenancy" + mock_response = MagicMock() + mock_response.data = mock_tenancy + + mock_client_instance = MagicMock() + mock_client_instance.get_tenancy.return_value = mock_response + mock_identity_client.return_value = mock_client_instance + + identity = OraclecloudProvider.set_identity(mock_session) + + assert identity.tenancy_name == "test-tenancy" + assert identity.tenancy_id == "ocid1.tenancy.oc1..aaaaaaaexample" + assert identity.user_id == "ocid1.user.oc1..aaaaaaaexample" + assert identity.region == "us-ashburn-1" + + @staticmethod + def _create_service_error(status, message): + """Helper to create an OCI ServiceError.""" + import oci + + error = oci.exceptions.ServiceError( + status=status, + code="TestError", + headers={}, + message=message, + ) + return error + + +class TestTestConnectionKeyValidation: + """Tests for key_content validation in test_connection()""" + + def test_test_connection_invalid_base64_key_raises_error(self): + """Test invalid base64 key content raises OCIInvalidConfigError.""" + with pytest.raises(OCIInvalidConfigError) as exc_info: + OraclecloudProvider.test_connection( + oci_config_file=None, + profile=None, + key_content="not-valid-base64!!!", + user="ocid1.user.oc1..aaaaaaaexample", + fingerprint="aa:bb:cc:dd:ee:ff:00:11:22:33:44:55:66:77:88:99", + tenancy="ocid1.tenancy.oc1..aaaaaaaexample", + region="us-ashburn-1", + ) + + assert "Failed to decode key_content" in str(exc_info.value) + + def test_test_connection_valid_key_content_proceeds(self): + """Test valid base64 key content proceeds to authentication.""" + import base64 + + # The SDK will validate the actual key format during authentication + valid_key = """-----BEGIN RSA PRIVATE KEY----- +MIIEpQIBAAKCAQEA0Z3VS5JJcds3xfn/ygWyF8n0sMcD/QHWCJ7yGSEtLN2T +...key content... +-----END RSA PRIVATE KEY-----""" + encoded_key = base64.b64encode(valid_key.encode("utf-8")).decode("utf-8") + + with ( + patch("oci.config.validate_config"), + patch("oci.identity.IdentityClient") as mock_identity_client, + ): + mock_tenancy = MagicMock() + mock_tenancy.name = "test-tenancy" + mock_response = MagicMock() + mock_response.data = mock_tenancy + + mock_client_instance = MagicMock() + mock_client_instance.get_tenancy.return_value = mock_response + mock_identity_client.return_value = mock_client_instance + + connection = OraclecloudProvider.test_connection( + oci_config_file=None, + profile=None, + key_content=encoded_key, + user="ocid1.user.oc1..aaaaaaaexample", + fingerprint="aa:bb:cc:dd:ee:ff:00:11:22:33:44:55:66:77:88:99", + tenancy="ocid1.tenancy.oc1..aaaaaaaexample", + region="us-ashburn-1", + raise_on_exception=False, + ) + + assert connection.is_connected is True diff --git a/ui/AGENTS.md b/ui/AGENTS.md index 5eada68033..0e26f60f6f 100644 --- a/ui/AGENTS.md +++ b/ui/AGENTS.md @@ -1,5 +1,17 @@ # Prowler UI - AI Agent Ruleset +> **Skills Reference**: For detailed patterns, use these skills: +> - [`prowler-ui`](../skills/prowler-ui/SKILL.md) - Prowler-specific UI patterns +> - [`prowler-test-ui`](../skills/prowler-test-ui/SKILL.md) - Playwright E2E testing (comprehensive) +> - [`typescript`](../skills/typescript/SKILL.md) - Const types, flat interfaces +> - [`react-19`](../skills/react-19/SKILL.md) - No useMemo/useCallback, compiler +> - [`nextjs-15`](../skills/nextjs-15/SKILL.md) - App Router, Server Actions +> - [`tailwind-4`](../skills/tailwind-4/SKILL.md) - cn() utility, no var() in className +> - [`zod-4`](../skills/zod-4/SKILL.md) - New API (z.email(), z.uuid()) +> - [`zustand-5`](../skills/zustand-5/SKILL.md) - Selectors, persist middleware +> - [`ai-sdk-5`](../skills/ai-sdk-5/SKILL.md) - UIMessage, sendMessage +> - [`playwright`](../skills/playwright/SKILL.md) - Page Object Model, selectors + ## CRITICAL RULES - NON-NEGOTIABLE ### React @@ -19,48 +31,19 @@ - ALWAYS: Reuse via `extends` - NEVER: Inline nested objects -```typescript -// ✅ CORRECT -interface UserAddress { - street: string; - city: string; -} -interface User { - id: string; - address: UserAddress; -} -interface Admin extends User { - permissions: string[]; -} - -// ❌ WRONG -interface User { - address: { street: string; city: string }; -} -``` - ### Styling - Single class: `className="bg-slate-800 text-white"` -- Merge multiple classes: `className={cn(BUTTON_STYLES.base, BUTTON_STYLES.active, isLoading && "opacity-50")}` (cn() handles Tailwind conflicts with twMerge) -- Conditional classes: `className={cn("base", condition && "variant")}` -- Recharts props: `fill={CHART_COLORS.text}` (use constants with var()) -- Dynamic values: `style={{ width: "50%", opacity: 0.5 }}` -- CSS custom properties: `style={{ "--color": "var(--css-var)" }}` (for dynamic theming) -- NEVER: `var()` in className strings (use Tailwind semantic classes instead) -- NEVER: hex colors (use `text-white` not `text-[#fff]`) +- Merge multiple classes: `className={cn(BASE_STYLES, variant && "variant-class")}` +- Dynamic values: `style={{ width: "50%" }}` +- NEVER: `var()` in className, hex colors ### Scope Rule (ABSOLUTE) -- Used 2+ places → `components/shared/` or `lib/` or `types/` or `hooks/` +- Used 2+ places → `lib/` or `types/` or `hooks/` (components go in `components/{domain}/`) - Used 1 place → keep local in feature directory - This determines ALL folder structure decisions -### Memoization - -- NEVER: `useMemo`, `useCallback` -- React 19 Compiler handles automatic optimization - --- ## DECISION TREES @@ -68,8 +51,8 @@ interface User { ### Component Placement ``` -New feature UI? → shadcn/ui + Tailwind | Existing feature? → HeroUI -Used 1 feature? → features/{feature}/components | Used 2+? → components/shared +New/Existing UI? → shadcn/ui + Tailwind (NEVER HeroUI for new code) +Used 1 feature? → features/{feature}/components | Used 2+? → components/{domain}/ Needs state/hooks? → "use client" | Server component? → No directive ``` @@ -81,15 +64,7 @@ Data transform → actions/{feature}/{feature}.adapter.ts Types (shared 2+) → types/{domain}.ts | Types (local 1) → {feature}/types.ts Utils (shared 2+) → lib/ | Utils (local 1) → {feature}/utils/ Hooks (shared 2+) → hooks/ | Hooks (local 1) → {feature}/hooks.ts -shadcn components → components/shadcn/ | HeroUI → components/ui/ -``` - -### Styling Decision - -``` -Tailwind class exists? → className | Dynamic value? → style prop -Conditional styles? → cn() | Static? → className only -Recharts? → CHART_COLORS constant + var() | Other? → Tailwind classes +shadcn components → components/shadcn/ ``` --- @@ -105,14 +80,6 @@ export default async function Page() { } ``` -### Form + Validation - -```typescript -import { useForm } from "react-hook-form"; -import { zodResolver } from "@hookform/resolvers/zod"; -const form = useForm({ resolver: zodResolver(schema) }); -``` - ### Server Action ```typescript @@ -124,15 +91,22 @@ export async function updateProvider(formData: FormData) { } ``` -### Zod v4 +### Form + Validation (Zod 4) -- `z.email()` not `z.string().email()` -- `z.uuid()` not `z.string().uuid()` -- `z.url()` not `z.string().url()` -- `z.string().min(1)` not `z.string().nonempty()` -- `error` param not `message` param +```typescript +import { useForm } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { z } from "zod"; -### Zustand v5 +const schema = z.object({ + email: z.email(), // Zod 4: z.email() not z.string().email() + id: z.uuid(), // Zod 4: z.uuid() not z.string().uuid() +}); + +const form = useForm({ resolver: zodResolver(schema) }); +``` + +### Zustand 5 ```typescript const useStore = create( @@ -146,54 +120,31 @@ const useStore = create( ); ``` -### AI SDK v5 - -```typescript -import { useChat } from "@ai-sdk/react"; -const { messages, sendMessage } = useChat({ - transport: new DefaultChatTransport({ api: "/api/chat" }), -}); -const [input, setInput] = useState(""); -const handleSubmit = (e) => { - e.preventDefault(); - sendMessage({ text: input }); - setInput(""); -}; -``` - -### Testing (Playwright) +### Playwright Test ```typescript export class FeaturePage extends BasePage { readonly submitBtn = this.page.getByRole("button", { name: "Submit" }); - async goto() { - await super.goto("/path"); - } - async submit() { - await this.submitBtn.click(); - } + async goto() { await super.goto("/path"); } + async submit() { await this.submitBtn.click(); } } -test( - "action works", - { tag: ["@critical", "@feature", "@TEST-001"] }, - async ({ page }) => { - const p = new FeaturePage(page); - await p.goto(); - await p.submit(); - await expect(page).toHaveURL("/expected"); - }, -); +test("action works", { tag: ["@critical", "@feature"] }, async ({ page }) => { + const p = new FeaturePage(page); + await p.goto(); + await p.submit(); + await expect(page).toHaveURL("/expected"); +}); ``` -Selector priority: `getByRole()` → `getByLabel()` → `getByText()` → other - --- ## TECH STACK -Next.js 15.5.3 | React 19.1.1 | Tailwind 4.1.13 | shadcn/ui (new) | HeroUI 2.8.4 (legacy) -Zod 4.1.11 | React Hook Form 7.62.0 | Zustand 5.0.8 | NextAuth 5.0.0-beta.29 | Recharts 2.15.4 +Next.js 15.5.9 | React 19.2.2 | Tailwind 4.1.13 | shadcn/ui +Zod 4.1.11 | React Hook Form 7.62.0 | Zustand 5.0.8 | NextAuth 5.0.0-beta.30 | Recharts 2.15.4 + +> **Note**: HeroUI exists in `components/ui/` as legacy code. Do NOT add new components there. --- @@ -201,36 +152,30 @@ Zod 4.1.11 | React Hook Form 7.62.0 | Zustand 5.0.8 | NextAuth 5.0.0-beta.29 | R ``` ui/ -├── app/ (Next.js App Router) -│ ├── (auth)/ (Auth pages) -│ └── (prowler)/ (Main app: compliance, findings, providers, scans, services, integrations) -├── components/ -│ ├── shadcn/ (New shadcn/ui components) -│ ├── ui/ (HeroUI base) -│ └── {domain}/ (Domain components) -├── actions/ (Server actions) -├── types/ (Shared types) -├── hooks/ (Shared hooks) -├── lib/ (Utilities) -├── store/ (Zustand state) -├── tests/ (Playwright E2E) -└── styles/ (Global CSS) +├── app/(auth)/ # Auth pages +├── app/(prowler)/ # Main app: compliance, findings, providers, scans +├── components/shadcn/ # shadcn/ui components (USE THIS) +├── components/ui/ # HeroUI (LEGACY - do not add here) +├── actions/ # Server actions +├── types/ # Shared types +├── hooks/ # Shared hooks +├── lib/ # Utilities +├── store/ # Zustand state +├── tests/ # Playwright E2E +└── styles/ # Global CSS ``` --- ## COMMANDS -``` -pnpm install && pnpm run dev (Setup & start) -pnpm run typecheck (Type check) -pnpm run lint:fix (Fix linting) -pnpm run format:write (Format) -pnpm run healthcheck (typecheck + lint) -pnpm run test:e2e (E2E tests) -pnpm run test:e2e:ui (E2E with UI) -pnpm run test:e2e:debug (Debug E2E) -pnpm run build && pnpm start (Build & start) +```bash +pnpm install && pnpm run dev +pnpm run typecheck +pnpm run lint:fix +pnpm run healthcheck +pnpm run test:e2e +pnpm run test:e2e:ui ``` --- @@ -245,13 +190,3 @@ pnpm run build && pnpm start (Build & start) - [ ] No secrets in code (use `.env.local`) - [ ] Error messages sanitized - [ ] Server-side validation present - ---- - -## MIGRATIONS (As of Jan 2025) - -React 18 → 19.1.1 (async components, compiler) -Next.js 14 → 15.5.3 -NextUI → HeroUI 2.8.4 -Zod 3 → 4 (see patterns section) -AI SDK 4 → 5 (see patterns section) diff --git a/ui/CHANGELOG.md b/ui/CHANGELOG.md index 29ba43583b..4aa46e86cb 100644 --- a/ui/CHANGELOG.md +++ b/ui/CHANGELOG.md @@ -7,6 +7,7 @@ All notable changes to the **Prowler UI** are documented in this file. ### 🚀 Added - Add search bar when adding a provider [(#9634)](https://github.com/prowler-cloud/prowler/pull/9634) +- New findings table UI with new design system components, improved filtering UX, and enhanced table interactions [(#9699)](https://github.com/prowler-cloud/prowler/pull/9699) - Add gradient background to Risk Plot for visual risk context [(#9664)](https://github.com/prowler-cloud/prowler/pull/9664) ### 🔄 Changed diff --git a/ui/app/(prowler)/_overview/_components/accounts-selector.tsx b/ui/app/(prowler)/_overview/_components/accounts-selector.tsx index 80a22b1379..dce5f049dc 100644 --- a/ui/app/(prowler)/_overview/_components/accounts-selector.tsx +++ b/ui/app/(prowler)/_overview/_components/accounts-selector.tsx @@ -45,7 +45,8 @@ export function AccountsSelector({ providers }: AccountsSelectorProps) { const router = useRouter(); const searchParams = useSearchParams(); - const current = searchParams.get("filter[provider_id__in]") || ""; + const filterKey = "filter[provider_id__in]"; + const current = searchParams.get(filterKey) || ""; const selectedTypes = searchParams.get("filter[provider_type__in]") || ""; const selectedTypesList = selectedTypes ? selectedTypes.split(",").filter(Boolean) @@ -61,10 +62,10 @@ export function AccountsSelector({ providers }: AccountsSelectorProps) { const handleMultiValueChange = (ids: string[]) => { const params = new URLSearchParams(searchParams.toString()); + params.delete(filterKey); + if (ids.length > 0) { - params.set("filter[provider_id__in]", ids.join(",")); - } else { - params.delete("filter[provider_id__in]"); + params.set(filterKey, ids.join(",")); } // Auto-deselect provider types that no longer have any selected accounts diff --git a/ui/app/(prowler)/_overview/attack-surface/_components/attack-surface-card-item.tsx b/ui/app/(prowler)/_overview/attack-surface/_components/attack-surface-card-item.tsx index 9a26763c3e..46f2b0dd8e 100644 --- a/ui/app/(prowler)/_overview/attack-surface/_components/attack-surface-card-item.tsx +++ b/ui/app/(prowler)/_overview/attack-surface/_components/attack-surface-card-item.tsx @@ -2,7 +2,6 @@ import Link from "next/link"; import { AttackSurfaceItem } from "@/actions/overview"; import { Card, CardContent } from "@/components/shadcn"; -import { mapProviderFiltersForFindings } from "@/lib"; interface AttackSurfaceCardItemProps { item: AttackSurfaceItem; @@ -29,9 +28,6 @@ export function AttackSurfaceCardItem({ } }); - // Map provider filters for findings page compatibility - mapProviderFiltersForFindings(params); - return `/findings?${params.toString()}`; }; diff --git a/ui/app/(prowler)/_overview/graphs-tabs/findings-view/findings-view.ssr.tsx b/ui/app/(prowler)/_overview/graphs-tabs/findings-view/findings-view.ssr.tsx index 7dcbac50e9..c71e74c623 100644 --- a/ui/app/(prowler)/_overview/graphs-tabs/findings-view/findings-view.ssr.tsx +++ b/ui/app/(prowler)/_overview/graphs-tabs/findings-view/findings-view.ssr.tsx @@ -8,7 +8,6 @@ import { LinkToFindings } from "@/components/overview"; import { ColumnNewFindingsToDate } from "@/components/overview/new-findings-table/table/column-new-findings-to-date"; import { DataTable } from "@/components/ui/table"; import { createDict } from "@/lib/helper"; -import { mapProviderFiltersForFindingsObject } from "@/lib/provider-helpers"; import { FindingProps, SearchParamsProps } from "@/types"; import { pickFilterParams } from "../../_lib/filter-params"; @@ -27,8 +26,7 @@ export async function FindingsViewSSR({ searchParams }: FindingsViewSSRProps) { }; const filters = pickFilterParams(searchParams); - const mappedFilters = mapProviderFiltersForFindingsObject(filters); - const combinedFilters = { ...defaultFilters, ...mappedFilters }; + const combinedFilters = { ...defaultFilters, ...filters }; const findingsData = await getLatestFindings({ query: undefined, diff --git a/ui/app/(prowler)/_overview/graphs-tabs/risk-plot/risk-plot-client.tsx b/ui/app/(prowler)/_overview/graphs-tabs/risk-plot/risk-plot-client.tsx index dc312762ea..84405fe0f1 100644 --- a/ui/app/(prowler)/_overview/graphs-tabs/risk-plot/risk-plot-client.tsx +++ b/ui/app/(prowler)/_overview/graphs-tabs/risk-plot/risk-plot-client.tsx @@ -8,7 +8,6 @@ import { HorizontalBarChart } from "@/components/graphs/horizontal-bar-chart"; import { ScatterPlot } from "@/components/graphs/scatter-plot"; import { AlertPill } from "@/components/graphs/shared/alert-pill"; import type { BarDataPoint } from "@/components/graphs/types"; -import { mapProviderFiltersForFindings } from "@/lib/provider-helpers"; import { SEVERITY_FILTER_MAP } from "@/types/severities"; // Score color thresholds (0-100 scale, higher = better) @@ -42,9 +41,6 @@ export function RiskPlotClient({ data }: RiskPlotClientProps) { // Build the URL with current filters const params = new URLSearchParams(searchParams.toString()); - // Transform provider filters (provider_id__in -> provider__in) - mapProviderFiltersForFindings(params); - // Add severity filter const severity = SEVERITY_FILTER_MAP[dataPoint.name]; if (severity) { @@ -52,7 +48,7 @@ export function RiskPlotClient({ data }: RiskPlotClientProps) { } // Add provider filter for the selected point - params.set("filter[provider__in]", selectedPoint.providerId); + params.set("filter[provider_id__in]", selectedPoint.providerId); // Add exclude muted findings filter params.set("filter[muted]", "false"); diff --git a/ui/app/(prowler)/_overview/risk-severity/_components/risk-severity-chart.tsx b/ui/app/(prowler)/_overview/risk-severity/_components/risk-severity-chart.tsx index e718168698..9a8b11cd8f 100644 --- a/ui/app/(prowler)/_overview/risk-severity/_components/risk-severity-chart.tsx +++ b/ui/app/(prowler)/_overview/risk-severity/_components/risk-severity-chart.tsx @@ -5,7 +5,6 @@ import { useRouter, useSearchParams } from "next/navigation"; import { HorizontalBarChart } from "@/components/graphs/horizontal-bar-chart"; import { BarDataPoint } from "@/components/graphs/types"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/shadcn"; -import { mapProviderFiltersForFindings } from "@/lib/provider-helpers"; import { calculatePercentage } from "@/lib/utils"; import { SEVERITY_FILTER_MAP } from "@/types/severities"; @@ -31,8 +30,6 @@ export const RiskSeverityChart = ({ // Build the URL with current filters plus severity and muted const params = new URLSearchParams(searchParams.toString()); - mapProviderFiltersForFindings(params); - const severity = SEVERITY_FILTER_MAP[dataPoint.name]; if (severity) { params.set("filter[severity__in]", severity); diff --git a/ui/app/(prowler)/_overview/severity-over-time/_components/finding-severity-over-time.tsx b/ui/app/(prowler)/_overview/severity-over-time/_components/finding-severity-over-time.tsx index 3990f52b31..619e8631fd 100644 --- a/ui/app/(prowler)/_overview/severity-over-time/_components/finding-severity-over-time.tsx +++ b/ui/app/(prowler)/_overview/severity-over-time/_components/finding-severity-over-time.tsx @@ -66,7 +66,7 @@ export const FindingSeverityOverTime = ({ params.set("filter[provider_type__in]", providerType); } if (providerId) { - params.set("filter[provider__in]", providerId); + params.set("filter[provider_id__in]", providerId); } router.push(`/findings?${params.toString()}`); diff --git a/ui/app/(prowler)/_overview/status-chart/_components/status-chart.tsx b/ui/app/(prowler)/_overview/status-chart/_components/status-chart.tsx index 4615410d80..8f6540ecb4 100644 --- a/ui/app/(prowler)/_overview/status-chart/_components/status-chart.tsx +++ b/ui/app/(prowler)/_overview/status-chart/_components/status-chart.tsx @@ -13,7 +13,6 @@ import { CardVariant, ResourceStatsCard, } from "@/components/shadcn"; -import { mapProviderFiltersForFindings } from "@/lib/provider-helpers"; import { calculatePercentage } from "@/lib/utils"; interface FindingsData { total: number; @@ -39,8 +38,6 @@ export const StatusChart = ({ // Build the URL with current filters plus status and muted const params = new URLSearchParams(searchParams.toString()); - mapProviderFiltersForFindings(params); - // Add status filter based on which segment was clicked if (dataPoint.name === "Fail Findings") { params.set("filter[status__in]", "FAIL"); diff --git a/ui/app/(prowler)/_overview/watchlist/_components/service-watchlist.tsx b/ui/app/(prowler)/_overview/watchlist/_components/service-watchlist.tsx index 71fc7e2146..b39adfbd2b 100644 --- a/ui/app/(prowler)/_overview/watchlist/_components/service-watchlist.tsx +++ b/ui/app/(prowler)/_overview/watchlist/_components/service-watchlist.tsx @@ -4,7 +4,6 @@ import { useRouter, useSearchParams } from "next/navigation"; import { useState } from "react"; import { ServiceOverview } from "@/actions/overview"; -import { mapProviderFiltersForFindings } from "@/lib/provider-helpers"; import { SortToggleButton } from "./sort-toggle-button"; import { WatchlistCard, WatchlistItem } from "./watchlist-card"; @@ -29,9 +28,6 @@ export const ServiceWatchlist = ({ items }: { items: ServiceOverview[] }) => { const handleItemClick = (item: WatchlistItem) => { const params = new URLSearchParams(searchParams.toString()); - - mapProviderFiltersForFindings(params); - params.set("filter[service__in]", item.key); params.set("filter[status__in]", "FAIL"); router.push(`/findings?${params.toString()}`); diff --git a/ui/app/(prowler)/findings/page.tsx b/ui/app/(prowler)/findings/page.tsx index 7fbb2e47c1..0818356f31 100644 --- a/ui/app/(prowler)/findings/page.tsx +++ b/ui/app/(prowler)/findings/page.tsx @@ -85,6 +85,7 @@ export default async function Findings({ return ( { + const searchParams = useSearchParams(); const { clearAllFilters, hasFilters } = useUrlFilters(); + // Count active filters (excluding search) + const filterCount = Array.from(searchParams.keys()).filter( + (key) => key.startsWith("filter[") && key !== "filter[search]", + ).length; + if (!hasFilters()) { return null; } + const displayText = showCount ? `Clear Filters (${filterCount})` : text; + return ( - ); }; diff --git a/ui/components/filters/custom-date-picker.tsx b/ui/components/filters/custom-date-picker.tsx index ff3a961d12..c2fcc8a432 100644 --- a/ui/components/filters/custom-date-picker.tsx +++ b/ui/components/filters/custom-date-picker.tsx @@ -1,120 +1,93 @@ "use client"; -import { Button, ButtonGroup } from "@heroui/button"; -import { DatePicker } from "@heroui/date-picker"; -import { - getLocalTimeZone, - parseDate, - startOfMonth, - startOfWeek, - today, -} from "@internationalized/date"; -import { useLocale } from "@react-aria/i18n"; -import type { DateValue } from "@react-types/datepicker"; +import { format } from "date-fns"; +import { CalendarIcon, ChevronDown } from "lucide-react"; import { useSearchParams } from "next/navigation"; -import { useEffect, useRef, useState } from "react"; +import { useEffect, useState } from "react"; +import { Calendar } from "@/components/shadcn/calendar"; +import { + Popover, + PopoverContent, + PopoverTrigger, +} from "@/components/shadcn/popover"; import { useUrlFilters } from "@/hooks/use-url-filters"; +import { cn } from "@/lib/utils"; export const CustomDatePicker = () => { const searchParams = useSearchParams(); const { updateFilter } = useUrlFilters(); + const [open, setOpen] = useState(false); - const [value, setValue] = useState(() => { + const [date, setDate] = useState(() => { const dateParam = searchParams.get("filter[inserted_at]"); - if (!dateParam) return null; + if (!dateParam) return undefined; try { - return parseDate(dateParam); + return new Date(dateParam); } catch { - return null; + return undefined; } }); - const { locale } = useLocale(); - - const now = today(getLocalTimeZone()); - const nextWeek = startOfWeek(now.add({ weeks: 1 }), locale); - const nextMonth = startOfMonth(now.add({ months: 1 })); - - const applyDateFilter = (date: DateValue | null) => { - if (date) { - updateFilter("inserted_at", date.toString()); + const applyDateFilter = (selectedDate: Date | undefined) => { + if (selectedDate) { + // Format as YYYY-MM-DD for the API + updateFilter("inserted_at", format(selectedDate, "yyyy-MM-dd")); } else { updateFilter("inserted_at", null); } }; - const initialRender = useRef(true); - + // Sync local state with URL params (e.g., when Clear Filters is clicked) useEffect(() => { - if (initialRender.current) { - initialRender.current = false; - return; - } - const params = new URLSearchParams(searchParams.toString()); - if (params.size === 0) { - setValue(null); + const dateParam = searchParams.get("filter[inserted_at]"); + if (!dateParam) { + setDate(undefined); + } else { + try { + setDate(new Date(dateParam)); + } catch { + setDate(undefined); + } } }, [searchParams]); - const handleDateChange = (newValue: DateValue | null) => { - setValue(newValue); - applyDateFilter(newValue); + const handleDateSelect = (newDate: Date | undefined) => { + setDate(newDate); + applyDateFilter(newDate); + setOpen(false); }; return ( -
- *]:!rounded-lg", - selectorButton: "text-bg-button-secondary shrink-0", - input: - "text-bg-button-secondary placeholder:text-bg-button-secondary text-sm", - innerWrapper: "[&]:!rounded-lg", - inputWrapper: - "!border-border-input-primary !bg-bg-input-primary dark:!bg-input/30 dark:hover:!bg-input/50 hover:!bg-bg-neutral-secondary !border [&]:!rounded-lg !shadow-xs !transition-[color,box-shadow] focus-within:!border-border-input-primary-press focus-within:!ring-1 focus-within:!ring-border-input-primary-press focus-within:!ring-offset-1 !h-10 !px-4 !py-3 !outline-none", - segment: "text-bg-button-secondary", - }} - popoverProps={{ - classNames: { - content: - "border-border-input-primary bg-bg-input-primary border rounded-lg", - }, - }} - CalendarTopContent={ - - - - - - } - calendarProps={{ - focusedValue: value || undefined, - onFocusChange: setValue, - nextButtonProps: { - variant: "bordered", - }, - prevButtonProps: { - variant: "bordered", - }, - }} - value={value} - onChange={handleDateChange} - /> -
+ + + + + + + + ); }; diff --git a/ui/components/filters/data-filters.ts b/ui/components/filters/data-filters.ts index 5464b09559..0baf89a3df 100644 --- a/ui/components/filters/data-filters.ts +++ b/ui/components/filters/data-filters.ts @@ -75,13 +75,6 @@ export const filterFindings = [ values: ["PASS", "FAIL", "MANUAL"], index: 1, }, - { - key: FilterType.PROVIDER_TYPE, - labelCheckboxGroup: "Cloud Provider", - values: [...PROVIDER_TYPES], - valueLabelMapping: PROVIDER_TYPE_MAPPING, - index: 5, - }, { key: FilterType.DELTA, labelCheckboxGroup: "Delta", diff --git a/ui/components/filters/filter-controls.tsx b/ui/components/filters/filter-controls.tsx index fa149fbcec..5099f05a2a 100644 --- a/ui/components/filters/filter-controls.tsx +++ b/ui/components/filters/filter-controls.tsx @@ -1,7 +1,6 @@ "use client"; import { Spacer } from "@heroui/spacer"; -import React from "react"; import { FilterOption } from "@/types"; @@ -23,7 +22,7 @@ export interface FilterControlsProps { customFilters?: FilterOption[]; } -export const FilterControls: React.FC = ({ +export const FilterControls = ({ search = false, providers = false, date = false, @@ -31,7 +30,7 @@ export const FilterControls: React.FC = ({ accounts = false, mutedFindings = false, customFilters, -}) => { +}: FilterControlsProps) => { return (
@@ -44,8 +43,12 @@ export const FilterControls: React.FC = ({ {mutedFindings && }
- - {customFilters && } + {customFilters && customFilters.length > 0 && ( + <> + + + + )}
); }; diff --git a/ui/components/findings/findings-filters.tsx b/ui/components/findings/findings-filters.tsx index 8bf51da877..571a976086 100644 --- a/ui/components/findings/findings-filters.tsx +++ b/ui/components/findings/findings-filters.tsx @@ -1,12 +1,25 @@ "use client"; +import { ChevronDown } from "lucide-react"; +import { useState } from "react"; + +import { AccountsSelector } from "@/app/(prowler)/_overview/_components/accounts-selector"; +import { ProviderTypeSelector } from "@/app/(prowler)/_overview/_components/provider-type-selector"; +import { ClearFiltersButton } from "@/components/filters/clear-filters-button"; +import { CustomCheckboxMutedFindings } from "@/components/filters/custom-checkbox-muted-findings"; +import { CustomDatePicker } from "@/components/filters/custom-date-picker"; import { filterFindings } from "@/components/filters/data-filters"; -import { FilterControls } from "@/components/filters/filter-controls"; +import { Button } from "@/components/shadcn"; +import { ExpandableSection } from "@/components/ui/expandable-section"; +import { DataTableFilterCustom } from "@/components/ui/table"; import { useRelatedFilters } from "@/hooks"; import { getCategoryLabel } from "@/lib/categories"; import { FilterEntity, FilterType, ScanEntity, ScanProps } from "@/types"; +import { ProviderProps } from "@/types/providers"; interface FindingsFiltersProps { + /** Provider data for ProviderTypeSelector and AccountsSelector */ + providers: ProviderProps[]; providerIds: string[]; providerDetails: { [id: string]: FilterEntity }[]; completedScans: ScanProps[]; @@ -19,6 +32,7 @@ interface FindingsFiltersProps { } export const FindingsFilters = ({ + providers, providerIds, providerDetails, completedScanIds, @@ -28,7 +42,9 @@ export const FindingsFilters = ({ uniqueResourceTypes, uniqueCategories, }: FindingsFiltersProps) => { - const { availableProviderIds, availableScans } = useRelatedFilters({ + const [isExpanded, setIsExpanded] = useState(false); + + const { availableScans } = useRelatedFilters({ providerIds, providerDetails, completedScanIds, @@ -36,55 +52,81 @@ export const FindingsFilters = ({ enableScanRelation: true, }); + // Custom filters for the expandable section (removed Provider - now using AccountsSelector) + const customFilters = [ + ...filterFindings, + { + key: FilterType.REGION, + labelCheckboxGroup: "Regions", + values: uniqueRegions, + index: 3, + }, + { + key: FilterType.SERVICE, + labelCheckboxGroup: "Services", + values: uniqueServices, + index: 4, + }, + { + key: FilterType.RESOURCE_TYPE, + labelCheckboxGroup: "Resource Type", + values: uniqueResourceTypes, + index: 8, + }, + { + key: FilterType.CATEGORY, + labelCheckboxGroup: "Category", + values: uniqueCategories, + labelFormatter: getCategoryLabel, + index: 5, + }, + { + key: FilterType.SCAN, + labelCheckboxGroup: "Scan ID", + values: availableScans, + valueLabelMapping: scanDetails, + index: 7, + }, + ]; + + const hasCustomFilters = customFilters.length > 0; + return ( - <> - - +
+ {/* First row: Provider selectors + Muted checkbox + More Filters button + Clear Filters */} +
+
+ +
+
+ +
+ + {hasCustomFilters && ( + + )} + +
+ + {/* Expandable filters section */} + {hasCustomFilters && ( + + } + hideClearButton + /> + + )} +
); }; diff --git a/ui/components/findings/table/column-findings.tsx b/ui/components/findings/table/column-findings.tsx index f2cbbbd074..dca9087745 100644 --- a/ui/components/findings/table/column-findings.tsx +++ b/ui/components/findings/table/column-findings.tsx @@ -2,25 +2,14 @@ import { ColumnDef, RowSelectionState } from "@tanstack/react-table"; import { Database } from "lucide-react"; -import Link from "next/link"; import { useSearchParams } from "next/navigation"; -import { DataTableRowDetails } from "@/components/findings/table"; -import { DataTableRowActions } from "@/components/findings/table/data-table-row-actions"; -import { InfoIcon, MutedIcon } from "@/components/icons"; import { - Checkbox, - Tooltip, - TooltipContent, - TooltipProvider, - TooltipTrigger, -} from "@/components/shadcn"; -import { - DateWithTime, - EntityInfo, - SnippetChip, -} from "@/components/ui/entities"; -import { TriggerSheet } from "@/components/ui/sheet"; + DataTableRowActions, + FindingDetail, +} from "@/components/findings/table"; +import { Checkbox } from "@/components/shadcn"; +import { DateWithTime, SnippetChip } from "@/components/ui/entities"; import { DataTableColumnHeader, SeverityBadge, @@ -28,7 +17,10 @@ import { } from "@/components/ui/table"; import { FindingProps, ProviderType } from "@/types"; -import { DeltaIndicator } from "./delta-indicator"; +// TODO: PROWLER-379 - Enable ImpactedResourcesCell when backend supports grouped findings +// import { ImpactedResourcesCell } from "./impacted-resources-cell"; +import { DeltaValues, NotificationIndicator } from "./notification-indicator"; +import { ProviderIconCell } from "./provider-icon-cell"; const getFindingsData = (row: { original: FindingProps }) => { return row.original; @@ -58,10 +50,12 @@ const getProviderData = ( ); }; -const FindingDetailsCell = ({ row }: { row: any }) => { +// Component for finding title that opens the detail drawer +const FindingTitleCell = ({ row }: { row: { original: FindingProps } }) => { const searchParams = useSearchParams(); const findingId = searchParams.get("id"); const isOpen = findingId === row.original.id; + const { checktitle } = row.original.attributes.check_metadata; const handleOpenChange = (open: boolean) => { const params = new URLSearchParams(searchParams); @@ -76,22 +70,18 @@ const FindingDetailsCell = ({ row }: { row: any }) => { }; return ( -
- - } - title="Finding Details" - description="View the finding details" - defaultOpen={isOpen} - onOpenChange={handleOpenChange} - > - - -
+ +

+ {checktitle} +

+
+ } + /> ); }; @@ -106,11 +96,35 @@ export function getColumnFindings( selectedCount > 0 && selectedCount === selectableRowCount; const isSomeSelected = selectedCount > 0 && selectedCount < selectableRowCount; + return [ + // Notification column - shows new/changed/muted indicators + { + id: "notification", + header: () => null, + cell: ({ row }) => { + const finding = row.original; + const isMuted = finding.attributes.muted; + const mutedReason = finding.attributes.muted_reason; + const delta = finding.attributes.delta as + | (typeof DeltaValues)[keyof typeof DeltaValues] + | undefined; + + return ( + + ); + }, + enableSorting: false, + enableHiding: false, + }, + // Select column { id: "select", header: ({ table }) => { - // Use state calculated from rowSelection to force re-render const headerChecked = isAllSelected ? true : isSomeSelected @@ -118,14 +132,13 @@ export function getColumnFindings( : false; return ( -
+
table.toggleAllPageRowsSelected(checked === true) } aria-label="Select all" - // Disable when no rows are selectable (all muted) disabled={selectableRowCount === 0} />
@@ -134,46 +147,13 @@ export function getColumnFindings( cell: ({ row }) => { const finding = row.original; const isMuted = finding.attributes.muted; - const mutedReason = finding.attributes.muted_reason; - - // Show muted icon with tooltip for muted findings - if (isMuted) { - const ruleName = mutedReason || "Unknown rule"; - - return ( -
- - - -
- -
-
- - - - Mute rule: - - {ruleName} - - -
-
-
- ); - } - - // Use rowSelection directly instead of row.getIsSelected() - // This ensures re-render when selection state changes const isSelected = !!rowSelection[row.id]; return ( -
+
row.toggleSelected(checked === true) } @@ -185,41 +165,33 @@ export function getColumnFindings( enableSorting: false, enableHiding: false, }, + // Status column { - id: "moreInfo", + accessorKey: "status", header: ({ column }) => ( - + ), - cell: ({ row }) => , - enableSorting: false, + cell: ({ row }) => { + const { + attributes: { status }, + } = getFindingsData(row); + + return ; + }, }, + // Finding column - clickable to open detail sheet { accessorKey: "check", header: ({ column }) => ( ), - cell: ({ row }) => { - const { checktitle } = getFindingsMetadata(row); - const { delta } = row.original.attributes; - - return ( -
-
- {delta === "new" || delta === "changed" ? ( - - ) : null} -

- {checktitle} -

-
-
- ); - }, + cell: ({ row }) => , }, + // Resource name column { accessorKey: "resourceName", header: ({ column }) => ( @@ -238,12 +210,13 @@ export function getColumnFindings( }, enableSorting: false, }, + // Severity column { accessorKey: "severity", header: ({ column }) => ( ), @@ -254,74 +227,20 @@ export function getColumnFindings( return ; }, }, + // Provider column { - accessorKey: "status", + accessorKey: "provider", header: ({ column }) => ( - + ), cell: ({ row }) => { - const { - attributes: { status }, - } = getFindingsData(row); + const provider = getProviderData(row, "provider"); - return ; - }, - }, - { - accessorKey: "updated_at", - header: ({ column }) => ( - - ), - cell: ({ row }) => { - const { - attributes: { updated_at }, - } = getFindingsData(row); - return ( -
- -
- ); - }, - }, - // { - // accessorKey: "scanName", - // header: "Scan Name", - // cell: ({ row }) => { - // const name = getScanData(row, "name"); - - // return ( - //

- // {typeof name === "string" || typeof name === "number" - // ? name - // : "Invalid data"} - //

- // ); - // }, - // }, - { - accessorKey: "region", - header: ({ column }) => ( - - ), - cell: ({ row }) => { - const region = getResourceData(row, "region"); - - return ( -
- {typeof region === "string" ? region : "Invalid region"} -
- ); + return ; }, enableSorting: false, }, + // Service column { accessorKey: "service", header: ({ column }) => ( @@ -329,40 +248,47 @@ export function getColumnFindings( ), cell: ({ row }) => { const { servicename } = getFindingsMetadata(row); - return

{servicename}

; - }, - enableSorting: false, - }, - { - accessorKey: "cloudProvider", - header: ({ column }) => ( - - ), - cell: ({ row }) => { - const provider = getProviderData(row, "provider"); - const alias = getProviderData(row, "alias"); - const uid = getProviderData(row, "uid"); - return ( - <> - - +

+ {servicename} +

); }, enableSorting: false, }, + // TODO: PROWLER-379 - Enable Impacted Resources column when backend supports grouped findings + // { + // accessorKey: "impactedResources", + // header: ({ column }) => ( + // + // ), + // cell: () => { + // return ; + // }, + // enableSorting: false, + // }, + // Time column { - id: "actions", + accessorKey: "updated_at", header: ({ column }) => ( - + ), cell: ({ row }) => { - return ; + const { + attributes: { updated_at }, + } = getFindingsData(row); + return ; }, + }, + // Actions column - dropdown with Mute/Jira options + { + id: "actions", + header: () =>
, + cell: ({ row }) => , enableSorting: false, }, ]; diff --git a/ui/components/findings/table/data-table-row-actions.tsx b/ui/components/findings/table/data-table-row-actions.tsx index 585a4098d0..e465930413 100644 --- a/ui/components/findings/table/data-table-row-actions.tsx +++ b/ui/components/findings/table/data-table-row-actions.tsx @@ -16,7 +16,6 @@ import { MuteFindingsModal } from "@/components/findings/mute-findings-modal"; import { SendToJiraModal } from "@/components/findings/send-to-jira-modal"; import { VerticalDotsIcon } from "@/components/icons"; import { JiraIcon } from "@/components/icons/services/IconServices"; -import { Button } from "@/components/shadcn"; import type { FindingProps } from "@/types/components"; import { FindingsSelectionContext } from "./findings-selection-context"; @@ -92,22 +91,16 @@ export function DataTableRowActions({ row }: DataTableRowActionsProps) { onComplete={handleMuteComplete} /> -
+
- - + + { asChild > diff --git a/ui/components/findings/table/finding-detail.tsx b/ui/components/findings/table/finding-detail.tsx index 28a72fbe8c..62a2b17c37 100644 --- a/ui/components/findings/table/finding-detail.tsx +++ b/ui/components/findings/table/finding-detail.tsx @@ -1,22 +1,37 @@ "use client"; import { Snippet } from "@heroui/snippet"; -import { Tooltip } from "@heroui/tooltip"; -import { ExternalLink, Link } from "lucide-react"; +import { ExternalLink, Link, X } from "lucide-react"; +import { usePathname, useSearchParams } from "next/navigation"; +import type { ReactNode } from "react"; import ReactMarkdown from "react-markdown"; import { - Card, - CardAction, - CardContent, - CardHeader, - CardTitle, + Drawer, + DrawerClose, + DrawerContent, + DrawerDescription, + DrawerHeader, + DrawerTitle, + DrawerTrigger, + InfoField, + Tabs, + TabsContent, + TabsList, + TabsTrigger, + Tooltip, + TooltipContent, + TooltipTrigger, } from "@/components/shadcn"; import { CodeSnippet } from "@/components/ui/code-snippet/code-snippet"; import { CustomLink } from "@/components/ui/custom/custom-link"; -import { EntityInfo, InfoField } from "@/components/ui/entities"; +import { EntityInfo } from "@/components/ui/entities"; import { DateWithTime } from "@/components/ui/entities/date-with-time"; import { SeverityBadge } from "@/components/ui/table/severity-badge"; +import { + FindingStatus, + StatusFindingBadge, +} from "@/components/ui/table/status-finding-badge"; import { buildGitFileUrl, extractLineRangeFromUid } from "@/lib/iac-utils"; import { FindingProps, ProviderType } from "@/types"; @@ -50,20 +65,35 @@ const formatDuration = (seconds: number) => { return parts.join(" "); }; +interface FindingDetailProps { + findingDetails: FindingProps; + trigger?: ReactNode; + open?: boolean; + defaultOpen?: boolean; + onOpenChange?: (open: boolean) => void; +} + export const FindingDetail = ({ findingDetails, -}: { - findingDetails: FindingProps; -}) => { + trigger, + open, + defaultOpen = false, + onOpenChange, +}: FindingDetailProps) => { const finding = findingDetails; const attributes = finding.attributes; const resource = finding.relationships.resource.attributes; const scan = finding.relationships.scan.attributes; const providerDetails = finding.relationships.provider.attributes; - const currentUrl = new URL(window.location.href); - const params = new URLSearchParams(currentUrl.search); - params.set("id", findingDetails.id); - const url = `${window.location.origin}${currentUrl.pathname}?${params.toString()}`; + const pathname = usePathname(); + const searchParams = useSearchParams(); + + const copyFindingUrl = () => { + const params = new URLSearchParams(searchParams.toString()); + params.set("id", findingDetails.id); + const url = `${window.location.origin}${pathname}?${params.toString()}`; + navigator.clipboard.writeText(url); + }; // Build Git URL for IaC findings const gitUrl = @@ -76,49 +106,65 @@ export const FindingDetail = ({ ) : null; - return ( -
+ const content = ( +
{/* Header */} -
-
-

- {renderValue(attributes.check_metadata.checktitle)} - - - -

-
-
+
+ {/* Row 1: Status badges */} +
+ + + {attributes.delta && ( +
+ + + {attributes.delta} + +
+ )}
+ + {/* Row 2: Title with copy link */} +

+ {renderValue(attributes.check_metadata.checktitle)} + + + + + Copy finding link to clipboard + +

+ + {/* Row 3: First Seen */} +
+ +
- {/* Check Metadata */} - - - Finding Details -
- {renderValue(attributes.status)} -
-
- + {/* Tabs */} + + + General + Resources + Scans + + +

+ Here is an overview of this finding: +

+ + {/* General Tab */} +
{resource.region} - - +
+ +
+ + - {attributes.delta && ( - -
- - {attributes.delta} -
-
- )} - - + + + + +
- - - - - - - - - - - - {attributes.status === "FAIL" && ( @@ -189,7 +217,7 @@ export const FindingDetail = ({ {attributes.check_metadata.remediation && (
-

+

Remediation Details

@@ -267,30 +295,32 @@ export const FindingDetail = ({ {attributes.check_metadata.categories?.join(", ") || "none"} - - + - {/* Resource Details */} - - - Resource Details + {/* Resources Tab */} + {providerDetails.provider === "iac" && gitUrl && ( - - -
- - +
+ + + + + View in Repository + + + + Go to Resource in the Repository + - +
)} - - +
{renderValue(resource.name)} @@ -316,9 +346,13 @@ export const FindingDetail = ({
+ + + + {resource.tags && Object.entries(resource.tags).length > 0 && (
-

+

Tags

@@ -339,15 +373,10 @@ export const FindingDetail = ({
- - + - {/* Add new Scan Details section */} - - - Scan Details - - + {/* Scans Tab */} +
{scan.name || "N/A"} @@ -383,8 +412,36 @@ export const FindingDetail = ({ )}
-
-
+ +
); + + // If no trigger, render content directly (inline mode) + if (!trigger) { + return content; + } + + // With trigger, wrap in Drawer + return ( + + {trigger} + + + Finding Details + View the finding details + + + + Close + + {content} + + + ); }; diff --git a/ui/components/findings/table/findings-table-with-selection.tsx b/ui/components/findings/table/findings-table-with-selection.tsx index 0f3f5a9225..c3720758c9 100644 --- a/ui/components/findings/table/findings-table-with-selection.tsx +++ b/ui/components/findings/table/findings-table-with-selection.tsx @@ -97,6 +97,7 @@ export function FindingsTableWithSelection({ rowSelection={rowSelection} onRowSelectionChange={setRowSelection} getRowCanSelect={getRowCanSelect} + showSearch /> {selectedFindingIds.length > 0 && ( diff --git a/ui/components/findings/table/impacted-resources-cell.tsx b/ui/components/findings/table/impacted-resources-cell.tsx new file mode 100644 index 0000000000..8ad9ccd530 --- /dev/null +++ b/ui/components/findings/table/impacted-resources-cell.tsx @@ -0,0 +1,84 @@ +import { Check, Flag } from "lucide-react"; + +import { cn } from "@/lib/utils"; + +export const TriageStatusValues = { + IN_PROGRESS: "in_progress", + RESOLVED: "resolved", + NONE: "none", +} as const; + +export type TriageStatus = + (typeof TriageStatusValues)[keyof typeof TriageStatusValues]; + +interface TriageBadgeProps { + status: TriageStatus; + count: number; +} + +const TriageBadge = ({ status, count }: TriageBadgeProps) => { + if (status === TriageStatusValues.NONE) { + return null; + } + + const isInProgress = status === TriageStatusValues.IN_PROGRESS; + + return ( + + {isInProgress ? ( + + ) : ( + + )} + {count} + + {isInProgress ? "In-progress" : "Resolved"} + + + ); +}; + +interface ImpactedResourcesCellProps { + impacted: number; + total: number; + inProgress?: number; + resolved?: number; +} + +export const ImpactedResourcesCell = ({ + impacted, + total, + inProgress = 0, + resolved = 0, +}: ImpactedResourcesCellProps) => { + return ( +
+ + {impacted} + of + {total} + + + {inProgress > 0 && ( + + )} + + {resolved > 0 && ( + + )} +
+ ); +}; diff --git a/ui/components/findings/table/index.ts b/ui/components/findings/table/index.ts index de8098d501..e0c203e9e1 100644 --- a/ui/components/findings/table/index.ts +++ b/ui/components/findings/table/index.ts @@ -4,4 +4,8 @@ export * from "./data-table-row-details"; export * from "./finding-detail"; export * from "./findings-selection-context"; export * from "./findings-table-with-selection"; +// TODO: PROWLER-379 - Re-export when backend supports grouped findings +// export * from "./impacted-resources-cell"; +export * from "./notification-indicator"; +export * from "./provider-icon-cell"; export * from "./skeleton-table-findings"; diff --git a/ui/components/findings/table/notification-indicator.tsx b/ui/components/findings/table/notification-indicator.tsx new file mode 100644 index 0000000000..4e040a784d --- /dev/null +++ b/ui/components/findings/table/notification-indicator.tsx @@ -0,0 +1,100 @@ +import Link from "next/link"; + +import { MutedIcon } from "@/components/icons"; +import { Button } from "@/components/shadcn"; +import { + Tooltip, + TooltipContent, + TooltipTrigger, +} from "@/components/shadcn/tooltip"; +import { DOCS_URLS } from "@/lib/external-urls"; +import { cn } from "@/lib/utils"; + +export const DeltaValues = { + NEW: "new", + CHANGED: "changed", + NONE: "none", +} as const; + +export type DeltaType = (typeof DeltaValues)[keyof typeof DeltaValues]; + +interface NotificationIndicatorProps { + delta?: DeltaType; + isMuted?: boolean; + mutedReason?: string; +} + +export const NotificationIndicator = ({ + delta, + isMuted = false, + mutedReason, +}: NotificationIndicatorProps) => { + // Muted takes precedence over delta + if (isMuted) { + const ruleName = mutedReason || "Unknown rule"; + + return ( + + +
+ +
+
+ + + Mute rule: + {ruleName} + + +
+ ); + } + + // Show dot with tooltip for new or changed findings + if (delta === DeltaValues.NEW || delta === DeltaValues.CHANGED) { + return ( + + +
+ + +
+ + {delta === DeltaValues.NEW + ? "New finding." + : "Status changed since the previous scan."} + + +
+
+ + ); + } + + // No indicator - return minimal width placeholder + return
; +}; diff --git a/ui/components/findings/table/provider-icon-cell.tsx b/ui/components/findings/table/provider-icon-cell.tsx new file mode 100644 index 0000000000..c99d833cbc --- /dev/null +++ b/ui/components/findings/table/provider-icon-cell.tsx @@ -0,0 +1,52 @@ +import { + AlibabaCloudProviderBadge, + AWSProviderBadge, + AzureProviderBadge, + GCPProviderBadge, + GitHubProviderBadge, + IacProviderBadge, + KS8ProviderBadge, + M365ProviderBadge, + MongoDBAtlasProviderBadge, + OracleCloudProviderBadge, +} from "@/components/icons/providers-badge"; +import { ProviderType } from "@/types"; + +const PROVIDER_ICONS = { + aws: AWSProviderBadge, + azure: AzureProviderBadge, + gcp: GCPProviderBadge, + kubernetes: KS8ProviderBadge, + m365: M365ProviderBadge, + github: GitHubProviderBadge, + iac: IacProviderBadge, + oraclecloud: OracleCloudProviderBadge, + mongodbatlas: MongoDBAtlasProviderBadge, + alibabacloud: AlibabaCloudProviderBadge, +} as const; + +interface ProviderIconCellProps { + provider: ProviderType; + size?: number; +} + +export const ProviderIconCell = ({ + provider, + size = 26, +}: ProviderIconCellProps) => { + const IconComponent = PROVIDER_ICONS[provider]; + + if (!IconComponent) { + return ( +
+ ? +
+ ); + } + + return ( +
+ +
+ ); +}; diff --git a/ui/components/findings/table/skeleton-table-findings.tsx b/ui/components/findings/table/skeleton-table-findings.tsx index 4436f22f60..d7eccad760 100644 --- a/ui/components/findings/table/skeleton-table-findings.tsx +++ b/ui/components/findings/table/skeleton-table-findings.tsx @@ -1,39 +1,147 @@ -import React from "react"; - -import { Card } from "@/components/shadcn/card/card"; import { Skeleton } from "@/components/shadcn/skeleton/skeleton"; -export const SkeletonTableFindings = () => { - const columns = 7; - const rows = 4; - +const SkeletonTableRow = () => { return ( - - {/* Table headers */} -
- {Array.from({ length: columns }).map((_, index) => ( - - ))} -
- - {/* Table body */} -
- {Array.from({ length: rows }).map((_, rowIndex) => ( -
- {Array.from({ length: columns }).map((_, colIndex) => ( - - ))} -
- ))} -
-
+ + {/* Notification dot */} + +
+ +
+ + {/* Checkbox */} + +
+ + {/* Status */} + + + + {/* Finding - multiline text */} + +
+ + +
+ + {/* Resource name chip */} + +
+ + + +
+ + {/* Severity */} + +
+ + +
+ + {/* Provider icon */} + + + + {/* Service */} + + + + {/* Time */} + +
+ + +
+ + {/* Actions */} + + + + + ); +}; + +export const SkeletonTableFindings = () => { + const rows = 10; + + return ( +
+ {/* Toolbar: Search + Total entries */} +
+ {/* Search icon button */} + + {/* Total entries */} + +
+ + {/* Table */} + + + + {/* Notification - empty header */} + + {/* Status */} + + {/* Finding */} + + {/* Resource name */} + + {/* Severity */} + + {/* Provider */} + + {/* Service */} + + {/* Time */} + + {/* Actions - empty header */} + + + + {Array.from({ length: rows }).map((_, i) => ( + + ))} + +
+ {/* Checkbox */} + +
+
+ + + + + + + + + + + + + + +
+ + {/* Pagination */} +
+ {/* Rows per page */} +
+ + +
+ {/* Page info + navigation */} +
+ +
+ + + + +
+
+
+
); }; diff --git a/ui/components/graphs/sankey-chart.tsx b/ui/components/graphs/sankey-chart.tsx index a562f6a823..fe7708e7a1 100644 --- a/ui/components/graphs/sankey-chart.tsx +++ b/ui/components/graphs/sankey-chart.tsx @@ -7,7 +7,6 @@ import { Rectangle, ResponsiveContainer, Sankey, Tooltip } from "recharts"; import { PROVIDER_ICONS } from "@/components/icons/providers-badge"; import { initializeChartColors } from "@/lib/charts/colors"; -import { mapProviderFiltersForFindings } from "@/lib/provider-helpers"; import { PROVIDER_DISPLAY_NAMES } from "@/types/providers"; import { SEVERITY_FILTER_MAP } from "@/types/severities"; @@ -463,9 +462,6 @@ export function SankeyChart({ const severityFilter = SEVERITY_FILTER_MAP[nodeName]; if (severityFilter) { const params = new URLSearchParams(searchParams.toString()); - - mapProviderFiltersForFindings(params); - params.set("filter[severity__in]", severityFilter); params.set("filter[status__in]", "FAIL"); params.set("filter[muted]", "false"); @@ -480,12 +476,10 @@ export function SankeyChart({ if (severityFilter) { const params = new URLSearchParams(searchParams.toString()); - mapProviderFiltersForFindings(params); - // Always set provider_type filter based on the clicked link's source (provider) // This ensures clicking "AWS → High" filters by AWS even when no global filter is set - const hasProviderIdFilter = searchParams.has("filter[provider_id__in]"); - if (providerType && !hasProviderIdFilter) { + const hasProviderFilter = searchParams.has("filter[provider_id__in]"); + if (providerType && !hasProviderFilter) { params.set("filter[provider_type__in]", providerType); } diff --git a/ui/components/graphs/scatter-plot.tsx b/ui/components/graphs/scatter-plot.tsx index 21609d8078..598e1839f7 100644 --- a/ui/components/graphs/scatter-plot.tsx +++ b/ui/components/graphs/scatter-plot.tsx @@ -277,16 +277,12 @@ export function ScatterPlot({ const providers = Object.keys(dataByProvider); - // Calculate domain for ReferenceArea - // For X axis: get max value from data (using the correct dataKey) - // For Y axis: use domain if provided, otherwise calculate from data - const xDataValues = data.length > 0 ? data.map((d) => d[xAxis.dataKey]) : [0]; - const yDataValues = data.length > 0 ? data.map((d) => d[yAxis.dataKey]) : [0]; - + // ReferenceArea bounds - use very large values and let ifOverflow="hidden" clip to chart area + // This ensures the gradient always covers exactly the visible chart area regardless of data const minX = xAxis.domain?.[0] ?? 0; - const maxX = xAxis.domain?.[1] ?? Math.max(...xDataValues) * 1.031; + const maxX = xAxis.domain?.[1] ?? Number.MAX_SAFE_INTEGER; const minY = yAxis.domain?.[0] ?? 0; - const maxY = yAxis.domain?.[1] ?? Math.max(...yDataValues) * 1.031; + const maxY = yAxis.domain?.[1] ?? Number.MAX_SAFE_INTEGER; const handleSelectPoint = (point: T) => { if (onSelectPoint) { @@ -342,7 +338,7 @@ export function ScatterPlot({ y1={minY} y2={maxY} fill={`url(#${gradientId})`} - ifOverflow="extendDomain" + ifOverflow="hidden" /> )} { const params = new URLSearchParams(searchParams.toString()); - mapProviderFiltersForFindings(params); if (providerType) params.set("filter[provider_type__in]", providerType); params.set("filter[region__in]", regionCode); params.set("filter[status__in]", status); diff --git a/ui/components/providers/workflow/forms/test-connection-form.tsx b/ui/components/providers/workflow/forms/test-connection-form.tsx index fecced7c6b..d7f5344b61 100644 --- a/ui/components/providers/workflow/forms/test-connection-form.tsx +++ b/ui/components/providers/workflow/forms/test-connection-form.tsx @@ -4,6 +4,7 @@ import { Checkbox } from "@heroui/checkbox"; import { zodResolver } from "@hookform/resolvers/zod"; import { Icon } from "@iconify/react"; import { Loader2 } from "lucide-react"; +import Link from "next/link"; import { useRouter } from "next/navigation"; import { useState } from "react"; import { useForm } from "react-hook-form"; @@ -18,7 +19,6 @@ import { getTask } from "@/actions/task/tasks"; import { CheckIcon, RocketIcon } from "@/components/icons"; import { Button } from "@/components/shadcn"; import { useToast } from "@/components/ui"; -import { CustomLink } from "@/components/ui/custom/custom-link"; import { Form } from "@/components/ui/form"; import { checkTaskStatus } from "@/lib/helper"; import { ProviderType } from "@/types"; @@ -144,6 +144,11 @@ export const TestConnectionForm = ({ type: "server", message: data.error, }); + toast({ + variant: "destructive", + title: "Oops! Something went wrong", + description: data.error, + }); } else { setIsRedirecting(true); router.push("/scans"); @@ -239,8 +244,8 @@ export const TestConnectionForm = ({
{apiErrorMessage && ( -
-

{`Provider ID ${apiErrorMessage?.toLowerCase()}. Please check and try again.`}

+
+

{apiErrorMessage}

)} @@ -297,17 +302,9 @@ export const TestConnectionForm = ({
{apiErrorMessage ? ( - - - Back to providers - + ) : connectionStatus?.error ? ( + + {/* Expanded state - full input */} +
+
+ +
+ handleChange(e.target.value)} + onFocus={handleFocus} + onBlur={handleBlur} + className="border-border-neutral-tertiary bg-bg-neutral-tertiary focus:border-border-input-primary-pressed pr-9 pl-9 focus:ring-0 focus:ring-offset-0 [&::-webkit-search-cancel-button]:appearance-none [&::-webkit-search-decoration]:appearance-none [&::-webkit-search-results-button]:appearance-none [&::-webkit-search-results-decoration]:appearance-none" + /> + {isLoading && ( +
+ +
+ )} +
+
+ ); +}; diff --git a/ui/components/ui/table/data-table.tsx b/ui/components/ui/table/data-table.tsx index 4fb494b6d9..6420ff3cc2 100644 --- a/ui/components/ui/table/data-table.tsx +++ b/ui/components/ui/table/data-table.tsx @@ -25,6 +25,7 @@ import { TableRow, } from "@/components/ui/table"; import { DataTablePagination } from "@/components/ui/table/data-table-pagination"; +import { DataTableSearch } from "@/components/ui/table/data-table-search"; import { FilterOption, MetaDataProps } from "@/types"; interface DataTableProviderProps { @@ -38,6 +39,8 @@ interface DataTableProviderProps { onRowSelectionChange?: OnChangeFn; /** Function to determine if a row can be selected */ getRowCanSelect?: (row: Row) => boolean; + /** Show search bar in the table toolbar */ + showSearch?: boolean; } export function DataTable({ @@ -49,6 +52,7 @@ export function DataTable({ rowSelection, onRowSelectionChange, getRowCanSelect, + showSearch = false, }: DataTableProviderProps) { const [sorting, setSorting] = useState([]); const [columnFilters, setColumnFilters] = useState([]); @@ -79,66 +83,71 @@ export function DataTable({ ? Object.keys(rowSelection).filter((k) => rowSelection[k]).length : 0; + // Format total entries count + const totalEntries = metadata?.pagination?.count ?? 0; + const formattedTotal = totalEntries.toLocaleString(); + return ( - <> -
- - - {table.getHeaderGroups().map((headerGroup) => ( - - {headerGroup.headers.map((header) => { - return ( - - {header.isPlaceholder - ? null - : flexRender( - header.column.columnDef.header, - header.getContext(), - )} - - ); - })} - - ))} - - - {table.getRowModel().rows?.length ? ( - table.getRowModel().rows.map((row) => ( - - {row.getVisibleCells().map((cell) => ( - - {flexRender( - cell.column.columnDef.cell, - cell.getContext(), - )} - - ))} - - )) - ) : ( - - - No results. - - - )} - -
-
- {metadata && ( -
- +
+ {/* Table Toolbar */} + {(showSearch || metadata) && ( +
+
{showSearch && }
+ {metadata && ( + + {formattedTotal} Total Entries + + )}
)} - + + + {table.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header) => { + return ( + + {header.isPlaceholder + ? null + : flexRender( + header.column.columnDef.header, + header.getContext(), + )} + + ); + })} + + ))} + + + {table.getRowModel().rows?.length ? ( + table.getRowModel().rows.map((row) => ( + + {row.getVisibleCells().map((cell) => ( + + {flexRender(cell.column.columnDef.cell, cell.getContext())} + + ))} + + )) + ) : ( + + + No results. + + + )} + +
+ {metadata && ( + + )} +
); } diff --git a/ui/components/ui/table/index.ts b/ui/components/ui/table/index.ts index 48080e8ef7..908c1bb9d3 100644 --- a/ui/components/ui/table/index.ts +++ b/ui/components/ui/table/index.ts @@ -2,6 +2,7 @@ export * from "./data-table"; export * from "./data-table-column-header"; export * from "./data-table-filter-custom"; export * from "./data-table-pagination"; +export * from "./data-table-search"; export * from "./severity-badge"; export * from "./status-badge"; export * from "./status-finding-badge"; diff --git a/ui/components/ui/table/severity-badge.tsx b/ui/components/ui/table/severity-badge.tsx index eaaedf0c6e..b3072dffdb 100644 --- a/ui/components/ui/table/severity-badge.tsx +++ b/ui/components/ui/table/severity-badge.tsx @@ -1,53 +1,46 @@ -import { Chip } from "@heroui/chip"; -import clsx from "clsx"; -import React from "react"; +import { cn } from "@/lib/utils"; -import { AlertIcon } from "@/components/icons"; - -type Severity = "informational" | "low" | "medium" | "high" | "critical"; - -const severityIconMap = { - critical: , +export const SeverityValues = { + INFORMATIONAL: "informational", + LOW: "low", + MEDIUM: "medium", + HIGH: "high", + CRITICAL: "critical", } as const; -const getSeverityColor = ( - severity: Severity, -): "danger" | "warning" | "default" => { - switch (severity) { - case "critical": - return "danger"; - case "high": - return "danger"; - case "medium": - return "warning"; - case "low": - return "default"; - default: - return "default"; // this is a fallback, though unnecessary due to typing - } -}; +export type Severity = (typeof SeverityValues)[keyof typeof SeverityValues]; -const getSeverityIcon = (severity: Severity): React.ReactNode | null => { - return severity === "critical" ? severityIconMap.critical : null; -}; +const SEVERITY_CHIP_COLORS = { + critical: "bg-bg-data-critical", + high: "bg-bg-data-high", + medium: "bg-bg-data-medium", + low: "bg-bg-data-low", + informational: "bg-bg-data-info", +} as const; -export const SeverityBadge = ({ severity }: { severity: Severity }) => { - const color = getSeverityColor(severity); +const SEVERITY_DISPLAY_NAMES = { + critical: "Critical", + high: "High", + medium: "Medium", + low: "Low", + informational: "Info", +} as const; + +interface SeverityBadgeProps { + severity: Severity; +} + +export const SeverityBadge = ({ severity }: SeverityBadgeProps) => { + const chipColor = + SEVERITY_CHIP_COLORS[severity] || SEVERITY_CHIP_COLORS.informational; + const displayName = SEVERITY_DISPLAY_NAMES[severity] || severity; return ( - - - {severity} +
+
+ + {displayName} - +
); }; diff --git a/ui/components/ui/table/status-finding-badge.tsx b/ui/components/ui/table/status-finding-badge.tsx index 247f3c2b16..4dd53b8692 100644 --- a/ui/components/ui/table/status-finding-badge.tsx +++ b/ui/components/ui/table/status-finding-badge.tsx @@ -1,42 +1,47 @@ -import { Chip } from "@heroui/chip"; -import React from "react"; +import { cn } from "@/lib/utils"; -export type FindingStatus = "FAIL" | "PASS" | "MANUAL" | "MUTED"; +export const FindingStatusValues = { + FAIL: "FAIL", + PASS: "PASS", + MANUAL: "MANUAL", + MUTED: "MUTED", +} as const; -const statusColorMap: Record< - FindingStatus, - "danger" | "warning" | "success" | "default" -> = { - FAIL: "danger", - PASS: "success", - MANUAL: "warning", - MUTED: "default", -}; +export type FindingStatus = + (typeof FindingStatusValues)[keyof typeof FindingStatusValues]; -export const StatusFindingBadge = ({ - status, - size = "sm", - value, - ...props -}: { +const STATUS_STYLES = { + FAIL: "border-bg-fail text-bg-fail", + PASS: "border-bg-pass text-bg-pass", + MANUAL: "border-bg-warning text-bg-warning", + MUTED: "border-text-neutral-tertiary text-text-neutral-tertiary", +} as const; + +interface StatusFindingBadgeProps { status: FindingStatus; size?: "sm" | "md" | "lg"; value?: string | number; -}) => { - const color = statusColorMap[status]; +} + +export const StatusFindingBadge = ({ + status, + value, +}: StatusFindingBadgeProps) => { + const statusStyle = STATUS_STYLES[status] || STATUS_STYLES.MUTED; + const displayText = + status.charAt(0).toUpperCase() + status.slice(1).toLowerCase(); return ( - - - {status.charAt(0).toUpperCase() + status.slice(1).toLowerCase()} - {value !== undefined && `: ${value}`} - - + {displayText} + {value !== undefined && `: ${value}`} + ); }; diff --git a/ui/components/ui/table/table.tsx b/ui/components/ui/table/table.tsx index af4a8bcc37..9dff392a8f 100644 --- a/ui/components/ui/table/table.tsx +++ b/ui/components/ui/table/table.tsx @@ -1,23 +1,31 @@ -import { cn } from "@heroui/theme"; -import * as React from "react"; +import { + forwardRef, + HTMLAttributes, + TdHTMLAttributes, + ThHTMLAttributes, +} from "react"; -const Table = React.forwardRef< - HTMLTableElement, - React.HTMLAttributes ->(({ className, ...props }, ref) => ( -
- - -)); +import { cn } from "@/lib/utils"; + +const Table = forwardRef>( + ({ className, ...props }, ref) => ( +
+
+ + ), +); Table.displayName = "Table"; -const TableHeader = React.forwardRef< +const TableHeader = forwardRef< HTMLTableSectionElement, - React.HTMLAttributes + HTMLAttributes >(({ className, ...props }, ref) => ( + HTMLAttributes >(({ className, ...props }, ref) => ( )); TableBody.displayName = "TableBody"; -const TableFooter = React.forwardRef< +const TableFooter = forwardRef< HTMLTableSectionElement, - React.HTMLAttributes + HTMLAttributes >(({ className, ...props }, ref) => ( + HTMLAttributes >(({ className, ...props }, ref) => ( td:first-child]:rounded-l-full [&>td:last-child]:rounded-r-full", + "hover:bg-bg-neutral-tertiary", + "data-[state=selected]:bg-bg-neutral-tertiary", className, )} {...props} @@ -65,14 +76,21 @@ const TableRow = React.forwardRef< )); TableRow.displayName = "TableRow"; -const TableHead = React.forwardRef< +const TableHead = forwardRef< HTMLTableCellElement, - React.ThHTMLAttributes + ThHTMLAttributes >(({ className, ...props }, ref) => (
+ TdHTMLAttributes >(({ className, ...props }, ref) => ( + HTMLAttributes >(({ className, ...props }, ref) => (
{ switch (provider) { case "aws": diff --git a/ui/lib/provider-helpers.ts b/ui/lib/provider-helpers.ts index bb6336701a..3f5d9ceae2 100644 --- a/ui/lib/provider-helpers.ts +++ b/ui/lib/provider-helpers.ts @@ -5,46 +5,6 @@ import { ProviderType, } from "@/types/providers"; -/** - * Maps overview provider filters to findings page provider filters. - * Converts provider_id__in to provider__in and removes provider_type__in - * since provider__in is more specific. - */ -export const mapProviderFiltersForFindings = ( - params: URLSearchParams, -): void => { - const providerIds = params.get("filter[provider_id__in]"); - if (providerIds) { - params.delete("filter[provider_id__in]"); - params.delete("filter[provider_type__in]"); - params.set("filter[provider__in]", providerIds); - } -}; - -/** - * Maps overview provider filters to findings page provider filters (object version). - * Converts provider_id__in to provider__in and removes provider_type__in - * since provider__in is more specific. - */ -export const mapProviderFiltersForFindingsObject = < - T extends Record, ->( - filters: T, -): T => { - const result = { ...filters }; - const providerIdKey = "filter[provider_id__in]"; - const providerTypeKey = "filter[provider_type__in]"; - const providerKey = "filter[provider__in]"; - - if (providerIdKey in result) { - result[providerKey as keyof T] = result[providerIdKey as keyof T]; - delete result[providerIdKey as keyof T]; - delete result[providerTypeKey as keyof T]; - } - - return result; -}; - export const extractProviderUIDs = ( providersData: ProvidersApiResponse, ): string[] => { diff --git a/ui/package.json b/ui/package.json index 4b32d4e7b1..aac239f4d7 100644 --- a/ui/package.json +++ b/ui/package.json @@ -30,7 +30,7 @@ "@hookform/resolvers": "5.2.2", "@internationalized/date": "3.10.0", "@langchain/aws": "1.1.0", - "@langchain/core": "1.1.4", + "@langchain/core": "1.1.8", "@langchain/mcp-adapters": "1.0.3", "@langchain/openai": "1.1.3", "@next/third-parties": "15.5.9", @@ -84,6 +84,7 @@ "next-themes": "0.2.1", "radix-ui": "1.4.2", "react": "19.2.2", + "react-day-picker": "9.13.0", "react-dom": "19.2.2", "react-hook-form": "7.62.0", "react-markdown": "10.1.0", @@ -100,6 +101,7 @@ "tw-animate-css": "1.4.0", "use-stick-to-bottom": "1.1.1", "uuid": "11.1.0", + "vaul": "1.1.2", "world-atlas": "2.0.2", "zod": "4.1.11", "zustand": "5.0.8" diff --git a/ui/pnpm-lock.yaml b/ui/pnpm-lock.yaml index c3732824cb..9e38766354 100644 --- a/ui/pnpm-lock.yaml +++ b/ui/pnpm-lock.yaml @@ -35,16 +35,16 @@ importers: version: 3.10.0 '@langchain/aws': specifier: 1.1.0 - version: 1.1.0(@langchain/core@1.1.4(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@2.2.0(@opentelemetry/api@1.9.0))(openai@6.10.0(zod@4.1.11))) + version: 1.1.0(@langchain/core@1.1.8(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@2.2.0(@opentelemetry/api@1.9.0))(openai@6.10.0(zod@4.1.11))) '@langchain/core': - specifier: 1.1.4 - version: 1.1.4(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@2.2.0(@opentelemetry/api@1.9.0))(openai@6.10.0(zod@4.1.11)) + specifier: 1.1.8 + version: 1.1.8(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@2.2.0(@opentelemetry/api@1.9.0))(openai@6.10.0(zod@4.1.11)) '@langchain/mcp-adapters': specifier: 1.0.3 - version: 1.0.3(@cfworker/json-schema@4.1.1)(@langchain/core@1.1.4(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@2.2.0(@opentelemetry/api@1.9.0))(openai@6.10.0(zod@4.1.11)))(@langchain/langgraph@1.0.4(@langchain/core@1.1.4(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@2.2.0(@opentelemetry/api@1.9.0))(openai@6.10.0(zod@4.1.11)))(react-dom@19.2.2(react@19.2.2))(react@19.2.2)(zod-to-json-schema@3.25.0(zod@4.1.11))(zod@4.1.11)) + version: 1.0.3(@cfworker/json-schema@4.1.1)(@langchain/core@1.1.8(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@2.2.0(@opentelemetry/api@1.9.0))(openai@6.10.0(zod@4.1.11)))(@langchain/langgraph@1.0.4(@langchain/core@1.1.8(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@2.2.0(@opentelemetry/api@1.9.0))(openai@6.10.0(zod@4.1.11)))(react-dom@19.2.2(react@19.2.2))(react@19.2.2)(zod-to-json-schema@3.25.0(zod@4.1.11))(zod@4.1.11)) '@langchain/openai': specifier: 1.1.3 - version: 1.1.3(@langchain/core@1.1.4(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@2.2.0(@opentelemetry/api@1.9.0))(openai@6.10.0(zod@4.1.11))) + version: 1.1.3(@langchain/core@1.1.8(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@2.2.0(@opentelemetry/api@1.9.0))(openai@6.10.0(zod@4.1.11))) '@next/third-parties': specifier: 15.5.9 version: 15.5.9(next@15.5.9(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.56.1)(babel-plugin-react-compiler@19.1.0-rc.3)(react-dom@19.2.2(react@19.2.2))(react@19.2.2))(react@19.2.2) @@ -173,7 +173,7 @@ importers: version: 4.0.0 langchain: specifier: 1.1.5 - version: 1.1.5(@langchain/core@1.1.4(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@2.2.0(@opentelemetry/api@1.9.0))(openai@6.10.0(zod@4.1.11)))(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@2.2.0(@opentelemetry/api@1.9.0))(openai@6.10.0(zod@4.1.11))(react-dom@19.2.2(react@19.2.2))(react@19.2.2)(zod-to-json-schema@3.25.0(zod@4.1.11)) + version: 1.1.5(@langchain/core@1.1.8(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@2.2.0(@opentelemetry/api@1.9.0))(openai@6.10.0(zod@4.1.11)))(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@2.2.0(@opentelemetry/api@1.9.0))(openai@6.10.0(zod@4.1.11))(react-dom@19.2.2(react@19.2.2))(react@19.2.2)(zod-to-json-schema@3.25.0(zod@4.1.11)) lucide-react: specifier: 0.543.0 version: 0.543.0(react@19.2.2) @@ -198,6 +198,9 @@ importers: react: specifier: 19.2.2 version: 19.2.2 + react-day-picker: + specifier: 9.13.0 + version: 9.13.0(react@19.2.2) react-dom: specifier: 19.2.2 version: 19.2.2(react@19.2.2) @@ -246,6 +249,9 @@ importers: uuid: specifier: 11.1.0 version: 11.1.0 + vaul: + specifier: 1.1.2 + version: 1.1.2(@types/react-dom@19.1.9(@types/react@19.1.13))(@types/react@19.1.13)(react-dom@19.2.2(react@19.2.2))(react@19.2.2) world-atlas: specifier: 2.0.2 version: 2.0.2 @@ -723,6 +729,9 @@ packages: '@chevrotain/utils@11.0.3': resolution: {integrity: sha512-YslZMgtJUyuMbZ+aKvfF3x1f5liK4mWNxghFRv7jqRR9C3R3fAOGTTKvxXDa2Y1s9zSbcpuO0cAxDYsc9SrXoQ==} + '@date-fns/tz@1.4.1': + resolution: {integrity: sha512-P5LUNhtbj6YfI3iJjw5EL9eUAG6OitD0W3fWQcpQjDRc/QIsL0tRNuO1PcDvPccWL1fSTXXdE1ds+l95DV/OFA==} + '@dotenvx/dotenvx@1.51.2': resolution: {integrity: sha512-+693mNflujDZxudSEqSNGpn92QgFhJlBn9q2mDQ9yGWyHuz3hZ8B5g3EXCwdAz4DMJAI+OFCIbfEFZS+YRdrEA==} hasBin: true @@ -1702,8 +1711,8 @@ packages: peerDependencies: '@langchain/core': ^1.0.0 - '@langchain/core@1.1.4': - resolution: {integrity: sha512-AZVHVoLJzhHU/jsjeNto1pvfHaPxGT+V3PcVyvUw0kCiWftdu1bxfwhwSsZJ9B9iJeXJdCIUe089+NYd3FsEuw==} + '@langchain/core@1.1.8': + resolution: {integrity: sha512-kIUidOgc0ZdyXo4Ahn9Zas+OayqOfk4ZoKPi7XaDipNSWSApc2+QK5BVcjvwtzxstsNOrmXJiJWEN6WPF/MvAw==} engines: {node: '>=20'} '@langchain/langgraph-checkpoint@1.0.0': @@ -5215,6 +5224,9 @@ packages: resolution: {integrity: sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==} engines: {node: '>= 0.4'} + date-fns-jalali@4.1.0-0: + resolution: {integrity: sha512-hTIP/z+t+qKwBDcmmsnmjWTduxCg+5KfdqWQvb2X/8C9+knYY6epN/pfxdDuyVlSVeFz0sM5eEfwIUQ70U4ckg==} + date-fns@4.1.0: resolution: {integrity: sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==} @@ -6416,6 +6428,23 @@ packages: openai: optional: true + langsmith@0.4.2: + resolution: {integrity: sha512-BvBeFgSmR9esl8x5wsiDlALiHKKPybw2wE2Hh6x1tgSZki46H9c9KI9/06LARbPhyyDu/TZU7exfg6fnhdj1Qg==} + peerDependencies: + '@opentelemetry/api': '*' + '@opentelemetry/exporter-trace-otlp-proto': '*' + '@opentelemetry/sdk-trace-base': '*' + openai: '*' + peerDependenciesMeta: + '@opentelemetry/api': + optional: true + '@opentelemetry/exporter-trace-otlp-proto': + optional: true + '@opentelemetry/sdk-trace-base': + optional: true + openai: + optional: true + language-subtag-registry@0.3.23: resolution: {integrity: sha512-0K65Lea881pHotoGEa5gDlMxt3pctLi2RplBb7Ezh4rRdLEOtgi7n4EwK9lamnUCkKBqaeKRVebTq6BAxSkpXQ==} @@ -7386,6 +7415,12 @@ packages: resolution: {integrity: sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==} engines: {node: '>= 0.10'} + react-day-picker@9.13.0: + resolution: {integrity: sha512-euzj5Hlq+lOHqI53NiuNhCP8HWgsPf/bBAVijR50hNaY1XwjKjShAnIe8jm8RD2W9IJUvihDIZ+KrmqfFzNhFQ==} + engines: {node: '>=18'} + peerDependencies: + react: '>=16.8.0' + react-dom@19.2.2: resolution: {integrity: sha512-fhyD2BLrew6qYf4NNtHff1rLXvzR25rq49p+FeqByOazc6TcSi2n8EYulo5C1PbH+1uBW++5S1SG7FcUU6mlDg==} peerDependencies: @@ -8250,6 +8285,12 @@ packages: resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} engines: {node: '>= 0.8'} + vaul@1.1.2: + resolution: {integrity: sha512-ZFkClGpWyI2WUQjdLJ/BaGuV6AVQiJ3uELGk3OYtP+B6yCO7Cmn9vPFXVJkRaGkOJu3m8bQMgtyzNHixULceQA==} + peerDependencies: + react: ^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc + vfile-location@5.0.3: resolution: {integrity: sha512-5yXvWDEgqeiYiBe1lbxYF7UMAIm/IcopxMHrMQDq3nvKcjPKIhZklUKL+AE7J7uApI4kwe2snsK+eI6UTj9EHg==} @@ -8282,8 +8323,8 @@ packages: vscode-uri@3.0.8: resolution: {integrity: sha512-AyFQ0EVmsOZOlAnxoFOGOq1SQDWAB7C6aqMGS23svWAllfOaxbuFvcT8D1i8z3Gyn8fraVeZNNmN6e9bxxXkKw==} - watchpack@2.4.4: - resolution: {integrity: sha512-c5EGNOiyxxV5qmTtAB7rbiXxi1ooX1pQKMLX/MIabJjRA0SJBQOjKF+KSVfHkr9U1cADPon0mRiVe/riyaiDUA==} + watchpack@2.5.0: + resolution: {integrity: sha512-e6vZvY6xboSwLz2GD36c16+O/2Z6fKvIf4pOXptw2rY9MVwE/TXc6RGqxD3I3x0a28lwBY7DE+76uTPSsBrrCA==} engines: {node: '>=10.13.0'} web-namespaces@2.0.1: @@ -9231,6 +9272,8 @@ snapshots: '@chevrotain/utils@11.0.3': {} + '@date-fns/tz@1.4.1': {} + '@dotenvx/dotenvx@1.51.2': dependencies: commander: 11.1.0 @@ -10639,24 +10682,24 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.5 - '@langchain/aws@1.1.0(@langchain/core@1.1.4(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@2.2.0(@opentelemetry/api@1.9.0))(openai@6.10.0(zod@4.1.11)))': + '@langchain/aws@1.1.0(@langchain/core@1.1.8(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@2.2.0(@opentelemetry/api@1.9.0))(openai@6.10.0(zod@4.1.11)))': dependencies: '@aws-sdk/client-bedrock-agent-runtime': 3.948.0 '@aws-sdk/client-bedrock-runtime': 3.948.0 '@aws-sdk/client-kendra': 3.948.0 '@aws-sdk/credential-provider-node': 3.948.0 - '@langchain/core': 1.1.4(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@2.2.0(@opentelemetry/api@1.9.0))(openai@6.10.0(zod@4.1.11)) + '@langchain/core': 1.1.8(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@2.2.0(@opentelemetry/api@1.9.0))(openai@6.10.0(zod@4.1.11)) transitivePeerDependencies: - aws-crt - '@langchain/core@1.1.4(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@2.2.0(@opentelemetry/api@1.9.0))(openai@6.10.0(zod@4.1.11))': + '@langchain/core@1.1.8(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@2.2.0(@opentelemetry/api@1.9.0))(openai@6.10.0(zod@4.1.11))': dependencies: '@cfworker/json-schema': 4.1.1 ansi-styles: 5.2.0 camelcase: 6.3.0 decamelize: 1.2.0 js-tiktoken: 1.0.21 - langsmith: 0.3.87(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@2.2.0(@opentelemetry/api@1.9.0))(openai@6.10.0(zod@4.1.11)) + langsmith: 0.4.2(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@2.2.0(@opentelemetry/api@1.9.0))(openai@6.10.0(zod@4.1.11)) mustache: 4.2.0 p-queue: 6.6.2 uuid: 10.0.0 @@ -10667,26 +10710,26 @@ snapshots: - '@opentelemetry/sdk-trace-base' - openai - '@langchain/langgraph-checkpoint@1.0.0(@langchain/core@1.1.4(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@2.2.0(@opentelemetry/api@1.9.0))(openai@6.10.0(zod@4.1.11)))': + '@langchain/langgraph-checkpoint@1.0.0(@langchain/core@1.1.8(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@2.2.0(@opentelemetry/api@1.9.0))(openai@6.10.0(zod@4.1.11)))': dependencies: - '@langchain/core': 1.1.4(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@2.2.0(@opentelemetry/api@1.9.0))(openai@6.10.0(zod@4.1.11)) + '@langchain/core': 1.1.8(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@2.2.0(@opentelemetry/api@1.9.0))(openai@6.10.0(zod@4.1.11)) uuid: 10.0.0 - '@langchain/langgraph-sdk@1.2.0(@langchain/core@1.1.4(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@2.2.0(@opentelemetry/api@1.9.0))(openai@6.10.0(zod@4.1.11)))(react-dom@19.2.2(react@19.2.2))(react@19.2.2)': + '@langchain/langgraph-sdk@1.2.0(@langchain/core@1.1.8(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@2.2.0(@opentelemetry/api@1.9.0))(openai@6.10.0(zod@4.1.11)))(react-dom@19.2.2(react@19.2.2))(react@19.2.2)': dependencies: p-queue: 6.6.2 p-retry: 4.6.2 uuid: 9.0.1 optionalDependencies: - '@langchain/core': 1.1.4(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@2.2.0(@opentelemetry/api@1.9.0))(openai@6.10.0(zod@4.1.11)) + '@langchain/core': 1.1.8(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@2.2.0(@opentelemetry/api@1.9.0))(openai@6.10.0(zod@4.1.11)) react: 19.2.2 react-dom: 19.2.2(react@19.2.2) - '@langchain/langgraph@1.0.4(@langchain/core@1.1.4(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@2.2.0(@opentelemetry/api@1.9.0))(openai@6.10.0(zod@4.1.11)))(react-dom@19.2.2(react@19.2.2))(react@19.2.2)(zod-to-json-schema@3.25.0(zod@4.1.11))(zod@4.1.11)': + '@langchain/langgraph@1.0.4(@langchain/core@1.1.8(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@2.2.0(@opentelemetry/api@1.9.0))(openai@6.10.0(zod@4.1.11)))(react-dom@19.2.2(react@19.2.2))(react@19.2.2)(zod-to-json-schema@3.25.0(zod@4.1.11))(zod@4.1.11)': dependencies: - '@langchain/core': 1.1.4(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@2.2.0(@opentelemetry/api@1.9.0))(openai@6.10.0(zod@4.1.11)) - '@langchain/langgraph-checkpoint': 1.0.0(@langchain/core@1.1.4(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@2.2.0(@opentelemetry/api@1.9.0))(openai@6.10.0(zod@4.1.11))) - '@langchain/langgraph-sdk': 1.2.0(@langchain/core@1.1.4(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@2.2.0(@opentelemetry/api@1.9.0))(openai@6.10.0(zod@4.1.11)))(react-dom@19.2.2(react@19.2.2))(react@19.2.2) + '@langchain/core': 1.1.8(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@2.2.0(@opentelemetry/api@1.9.0))(openai@6.10.0(zod@4.1.11)) + '@langchain/langgraph-checkpoint': 1.0.0(@langchain/core@1.1.8(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@2.2.0(@opentelemetry/api@1.9.0))(openai@6.10.0(zod@4.1.11))) + '@langchain/langgraph-sdk': 1.2.0(@langchain/core@1.1.8(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@2.2.0(@opentelemetry/api@1.9.0))(openai@6.10.0(zod@4.1.11)))(react-dom@19.2.2(react@19.2.2))(react@19.2.2) uuid: 10.0.0 zod: 4.1.11 optionalDependencies: @@ -10695,10 +10738,10 @@ snapshots: - react - react-dom - '@langchain/mcp-adapters@1.0.3(@cfworker/json-schema@4.1.1)(@langchain/core@1.1.4(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@2.2.0(@opentelemetry/api@1.9.0))(openai@6.10.0(zod@4.1.11)))(@langchain/langgraph@1.0.4(@langchain/core@1.1.4(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@2.2.0(@opentelemetry/api@1.9.0))(openai@6.10.0(zod@4.1.11)))(react-dom@19.2.2(react@19.2.2))(react@19.2.2)(zod-to-json-schema@3.25.0(zod@4.1.11))(zod@4.1.11))': + '@langchain/mcp-adapters@1.0.3(@cfworker/json-schema@4.1.1)(@langchain/core@1.1.8(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@2.2.0(@opentelemetry/api@1.9.0))(openai@6.10.0(zod@4.1.11)))(@langchain/langgraph@1.0.4(@langchain/core@1.1.8(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@2.2.0(@opentelemetry/api@1.9.0))(openai@6.10.0(zod@4.1.11)))(react-dom@19.2.2(react@19.2.2))(react@19.2.2)(zod-to-json-schema@3.25.0(zod@4.1.11))(zod@4.1.11))': dependencies: - '@langchain/core': 1.1.4(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@2.2.0(@opentelemetry/api@1.9.0))(openai@6.10.0(zod@4.1.11)) - '@langchain/langgraph': 1.0.4(@langchain/core@1.1.4(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@2.2.0(@opentelemetry/api@1.9.0))(openai@6.10.0(zod@4.1.11)))(react-dom@19.2.2(react@19.2.2))(react@19.2.2)(zod-to-json-schema@3.25.0(zod@4.1.11))(zod@4.1.11) + '@langchain/core': 1.1.8(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@2.2.0(@opentelemetry/api@1.9.0))(openai@6.10.0(zod@4.1.11)) + '@langchain/langgraph': 1.0.4(@langchain/core@1.1.8(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@2.2.0(@opentelemetry/api@1.9.0))(openai@6.10.0(zod@4.1.11)))(react-dom@19.2.2(react@19.2.2))(react@19.2.2)(zod-to-json-schema@3.25.0(zod@4.1.11))(zod@4.1.11) '@modelcontextprotocol/sdk': 1.24.3(@cfworker/json-schema@4.1.1)(zod@4.1.11) debug: 4.4.3 zod: 4.1.11 @@ -10708,9 +10751,9 @@ snapshots: - '@cfworker/json-schema' - supports-color - '@langchain/openai@1.1.3(@langchain/core@1.1.4(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@2.2.0(@opentelemetry/api@1.9.0))(openai@6.10.0(zod@4.1.11)))': + '@langchain/openai@1.1.3(@langchain/core@1.1.8(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@2.2.0(@opentelemetry/api@1.9.0))(openai@6.10.0(zod@4.1.11)))': dependencies: - '@langchain/core': 1.1.4(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@2.2.0(@opentelemetry/api@1.9.0))(openai@6.10.0(zod@4.1.11)) + '@langchain/core': 1.1.8(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@2.2.0(@opentelemetry/api@1.9.0))(openai@6.10.0(zod@4.1.11)) js-tiktoken: 1.0.21 openai: 6.10.0(zod@4.1.11) zod: 4.1.11 @@ -14954,6 +14997,8 @@ snapshots: es-errors: 1.3.0 is-data-view: 1.0.2 + date-fns-jalali@4.1.0-0: {} + date-fns@4.1.0: {} dayjs@1.11.19: {} @@ -16319,11 +16364,11 @@ snapshots: kleur@4.1.5: {} - langchain@1.1.5(@langchain/core@1.1.4(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@2.2.0(@opentelemetry/api@1.9.0))(openai@6.10.0(zod@4.1.11)))(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@2.2.0(@opentelemetry/api@1.9.0))(openai@6.10.0(zod@4.1.11))(react-dom@19.2.2(react@19.2.2))(react@19.2.2)(zod-to-json-schema@3.25.0(zod@4.1.11)): + langchain@1.1.5(@langchain/core@1.1.8(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@2.2.0(@opentelemetry/api@1.9.0))(openai@6.10.0(zod@4.1.11)))(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@2.2.0(@opentelemetry/api@1.9.0))(openai@6.10.0(zod@4.1.11))(react-dom@19.2.2(react@19.2.2))(react@19.2.2)(zod-to-json-schema@3.25.0(zod@4.1.11)): dependencies: - '@langchain/core': 1.1.4(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@2.2.0(@opentelemetry/api@1.9.0))(openai@6.10.0(zod@4.1.11)) - '@langchain/langgraph': 1.0.4(@langchain/core@1.1.4(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@2.2.0(@opentelemetry/api@1.9.0))(openai@6.10.0(zod@4.1.11)))(react-dom@19.2.2(react@19.2.2))(react@19.2.2)(zod-to-json-schema@3.25.0(zod@4.1.11))(zod@4.1.11) - '@langchain/langgraph-checkpoint': 1.0.0(@langchain/core@1.1.4(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@2.2.0(@opentelemetry/api@1.9.0))(openai@6.10.0(zod@4.1.11))) + '@langchain/core': 1.1.8(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@2.2.0(@opentelemetry/api@1.9.0))(openai@6.10.0(zod@4.1.11)) + '@langchain/langgraph': 1.0.4(@langchain/core@1.1.8(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@2.2.0(@opentelemetry/api@1.9.0))(openai@6.10.0(zod@4.1.11)))(react-dom@19.2.2(react@19.2.2))(react@19.2.2)(zod-to-json-schema@3.25.0(zod@4.1.11))(zod@4.1.11) + '@langchain/langgraph-checkpoint': 1.0.0(@langchain/core@1.1.8(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@2.2.0(@opentelemetry/api@1.9.0))(openai@6.10.0(zod@4.1.11))) langsmith: 0.3.87(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@2.2.0(@opentelemetry/api@1.9.0))(openai@6.10.0(zod@4.1.11)) uuid: 10.0.0 zod: 4.1.11 @@ -16357,6 +16402,19 @@ snapshots: '@opentelemetry/sdk-trace-base': 2.2.0(@opentelemetry/api@1.9.0) openai: 6.10.0(zod@4.1.11) + langsmith@0.4.2(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@2.2.0(@opentelemetry/api@1.9.0))(openai@6.10.0(zod@4.1.11)): + dependencies: + '@types/uuid': 10.0.0 + chalk: 4.1.2 + console-table-printer: 2.15.0 + p-queue: 6.6.2 + semver: 7.7.3 + uuid: 10.0.0 + optionalDependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/sdk-trace-base': 2.2.0(@opentelemetry/api@1.9.0) + openai: 6.10.0(zod@4.1.11) + language-subtag-registry@0.3.23: {} language-tags@1.0.9: @@ -17508,6 +17566,13 @@ snapshots: iconv-lite: 0.7.1 unpipe: 1.0.0 + react-day-picker@9.13.0(react@19.2.2): + dependencies: + '@date-fns/tz': 1.4.1 + date-fns: 4.1.0 + date-fns-jalali: 4.1.0-0 + react: 19.2.2 + react-dom@19.2.2(react@19.2.2): dependencies: react: 19.2.2 @@ -18638,6 +18703,15 @@ snapshots: vary@1.1.2: {} + vaul@1.1.2(@types/react-dom@19.1.9(@types/react@19.1.13))(@types/react@19.1.13)(react-dom@19.2.2(react@19.2.2))(react@19.2.2): + dependencies: + '@radix-ui/react-dialog': 1.1.14(@types/react-dom@19.1.9(@types/react@19.1.13))(@types/react@19.1.13)(react-dom@19.2.2(react@19.2.2))(react@19.2.2) + react: 19.2.2 + react-dom: 19.2.2(react@19.2.2) + transitivePeerDependencies: + - '@types/react' + - '@types/react-dom' + vfile-location@5.0.3: dependencies: '@types/unist': 3.0.3 @@ -18687,7 +18761,7 @@ snapshots: vscode-uri@3.0.8: {} - watchpack@2.4.4: + watchpack@2.5.0: dependencies: glob-to-regexp: 0.4.1 graceful-fs: 4.2.11 @@ -18727,7 +18801,7 @@ snapshots: schema-utils: 4.3.3 tapable: 2.3.0 terser-webpack-plugin: 5.3.16(webpack@5.103.0) - watchpack: 2.4.4 + watchpack: 2.5.0 webpack-sources: 3.3.3 transitivePeerDependencies: - '@swc/core' diff --git a/ui/styles/globals.css b/ui/styles/globals.css index deeb8ee8c6..70c96f857d 100644 --- a/ui/styles/globals.css +++ b/ui/styles/globals.css @@ -28,6 +28,7 @@ --bg-input-primary: var(--color-white); --border-input-primary: var(--color-slate-400); --border-input-primary-press: var(--color-slate-700); + --border-input-primary-pressed: #A7F3D0; --border-input-primary-fill: var(--color-slate-500); /* Text Colors */ @@ -101,6 +102,7 @@ --bg-input-primary: var(--color-neutral-900); --border-input-primary: var(--color-neutral-800); --border-input-primary-press: var(--color-neutral-800); + --border-input-primary-pressed: #A7F3D0; --border-input-primary-fill: var(--color-neutral-300); /* Text Colors */ @@ -185,6 +187,7 @@ --color-bg-input-primary: var(--bg-input-primary); --color-border-input-primary: var(--border-input-primary); --color-border-input-primary-press: var(--border-input-primary-press); + --color-border-input-primary-pressed: var(--border-input-primary-pressed); --color-border-input-primary-fill: var(--border-input-primary-fill); /* Neutral Map Colors */